diff --git a/examples/button.rs b/examples/button.rs new file mode 100644 index 000000000..9d32819e8 --- /dev/null +++ b/examples/button.rs @@ -0,0 +1,11 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + button { "hello, desktop!" } + }) +} diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index c3ea4dc2c..7014a2771 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use std::{marker::PhantomData, panic::AssertUnwindSafe}; use crate::{ innerlude::Scoped, @@ -62,12 +62,19 @@ where } fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> { - let scope: &mut Scoped

= cx.bump().alloc(Scoped { - props: &self.props, - scope: cx, - }); + let res = std::panic::catch_unwind(AssertUnwindSafe(move || { + // Call the render function directly + let scope: &mut Scoped

= cx.bump().alloc(Scoped { + props: &self.props, + scope: cx, + }); - // Call the render function directly - (self.render_fn)(scope).into_return(cx) + (self.render_fn)(scope).into_return(cx) + })); + + match res { + Ok(e) => e, + Err(_) => RenderReturn::default(), + } } } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index a7d030162..aef8e8b2d 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -81,12 +81,12 @@ impl VirtualDom { self.ensure_drop_safety(id); if let Some(root) = self.scopes[id.0].as_ref().try_root_node() { - if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } { + if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } { self.drop_scope_inner(node) } } if let Some(root) = unsafe { self.scopes[id.0].as_ref().previous_frame().try_load_node() } { - if let RenderReturn::Sync(Some(node)) = unsafe { root.extend_lifetime_ref() } { + if let RenderReturn::Ready(node) = unsafe { root.extend_lifetime_ref() } { self.drop_scope_inner(node) } } @@ -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 423c923db..4e2d8f0a2 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 { @@ -236,10 +352,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, }); @@ -267,7 +383,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..], @@ -278,14 +394,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); } } @@ -298,9 +459,9 @@ impl<'b> VirtualDom { use DynamicNode::*; match node { Text(text) => self.create_dynamic_text(template, text, idx), - Fragment(frag) => self.create_fragment(frag), - Placeholder(frag) => self.create_placeholder(frag, template, idx), + Placeholder(place) => self.create_placeholder(place, template, idx), Component(component) => self.create_component_node(template, component, idx), + Fragment(frag) => frag.iter().map(|child| self.create(child)).sum(), } } @@ -311,7 +472,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)); @@ -322,7 +483,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, }); @@ -337,14 +498,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, }); @@ -352,40 +513,38 @@ impl<'b> VirtualDom { 0 } - pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize { - nodes.iter().map(|child| self.create(child)).sum() - } - pub(super) fn create_component_node( &mut self, template: &'b VNode<'b>, component: &'b VComponent<'b>, idx: usize, ) -> usize { - let scope = match component.props.take() { - Some(props) => { - let unbounded_props: Box = unsafe { std::mem::transmute(props) }; - let scope = self.new_scope(unbounded_props, component.name); - scope.id - } + use RenderReturn::*; - // Component is coming back, it probably still exists, right? - None => component.scope.get().unwrap(), - }; + // Load up a ScopeId for this vcomponent + let scope = self.load_scope_from_vcomponent(component); component.scope.set(Some(scope)); - let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() }; - - use RenderReturn::*; - - match return_nodes { - Sync(Some(t)) => self.mount_component(scope, template, t, idx), - Sync(None) => todo!("Propogate error upwards"), - Async(_) => self.mount_component_placeholder(template, idx, scope), + match unsafe { self.run_scope(scope).extend_lifetime_ref() } { + Ready(t) => self.mount_component(scope, template, t, idx), + Aborted(t) => self.mount_aborted(template, t), + Pending(_) => self.mount_async(template, idx, scope), } } + /// Load a scope from a vcomponent. If the props don't exist, that means the component is currently "live" + fn load_scope_from_vcomponent(&mut self, component: &VComponent) -> ScopeId { + component + .props + .take() + .map(|props| { + let unbounded_props: Box = unsafe { std::mem::transmute(props) }; + self.new_scope(unbounded_props, component.name).id + }) + .unwrap_or_else(|| component.scope.get().unwrap()) + } + fn mount_component( &mut self, scope: ScopeId, @@ -412,7 +571,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)); @@ -434,22 +593,24 @@ 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 } + fn mount_aborted(&mut self, parent: &'b VNode<'b>, placeholder: &VPlaceholder) -> usize { + let id = self.next_element(parent, &[]); + self.mutations.push(Mutation::CreatePlaceholder { id }); + placeholder.id.set(Some(id)); + 1 + } + /// Take the rendered nodes from a component and handle them if they were async /// /// IE simply assign an ID to the placeholder - fn mount_component_placeholder( - &mut self, - template: &VNode, - idx: usize, - scope: ScopeId, - ) -> usize { - let new_id = self.next_element(template, template.template.node_paths[idx]); + fn mount_async(&mut self, template: &VNode, idx: usize, scope: ScopeId) -> usize { + 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)); @@ -457,7 +618,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 @@ -469,24 +630,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 af262948e..f055fb650 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -30,31 +30,61 @@ impl<'b> VirtualDom { .try_load_node() .expect("Call rebuild before diffing"); - use RenderReturn::{Async, Sync}; + use RenderReturn::{Aborted, Pending, Ready}; match (old, new) { - (Sync(Some(l)), Sync(Some(r))) => self.diff_node(l, r), + // Normal pathway + (Ready(l), Ready(r)) => self.diff_node(l, r), - // Err cases - (Sync(Some(l)), Sync(None)) => self.diff_ok_to_err(l), - (Sync(None), Sync(Some(r))) => self.diff_err_to_ok(r), - (Sync(None), Sync(None)) => { /* nothing */ } + // Unwind the mutations if need be + (Ready(l), Aborted(p)) => self.diff_ok_to_err(l, p), - // Async - (Sync(Some(_l)), Async(_)) => todo!(), - (Sync(None), Async(_)) => todo!(), - (Async(_), Sync(Some(_r))) => todo!(), - (Async(_), Sync(None)) => { /* nothing */ } - (Async(_), Async(_)) => { /* nothing */ } + // Just move over the placeholder + (Aborted(l), Aborted(r)) => r.id.set(l.id.get()), + + // Becomes async, do nothing while we ait + (Ready(_nodes), Pending(_fut)) => self.diff_ok_to_async(_nodes, scope), + + // Placeholder becomes something + // We should also clear the error now + (Aborted(l), Ready(r)) => self.replace_placeholder(l, [r]), + + (Aborted(_), Pending(_)) => todo!("async should not resolve here"), + (Pending(_), Ready(_)) => todo!("async should not resolve here"), + (Pending(_), Aborted(_)) => todo!("async should not resolve here"), + (Pending(_), Pending(_)) => { + // All suspense should resolve before we diff it again + panic!("Should not roll from suspense to suspense."); + } }; } self.scope_stack.pop(); } - fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>) {} - fn diff_err_to_ok(&mut self, _l: &'b VNode<'b>) {} + fn diff_ok_to_async(&mut self, _new: &'b VNode<'b>, _scope: ScopeId) { + // + } + + fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _p: &'b VPlaceholder) { + todo!() + } 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 +129,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( @@ -118,7 +144,7 @@ impl<'b> VirtualDom { (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), (Placeholder(left), Placeholder(right)) => right.id.set(left.id.get()), (Component(left), Component(right)) => self.diff_vcomponent(left, right, node, idx), - (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, right), + (Placeholder(left), Fragment(right)) => self.replace_placeholder(left, *right), (Fragment(left), Placeholder(right)) => self.node_to_placeholder(left, right), _ => todo!("This is an usual custom case for dynamic nodes. We don't know how to handle it yet."), }; @@ -191,8 +217,12 @@ impl<'b> VirtualDom { ) { let m = self.create_component_node(right_template, right, idx); + let pre_edits = self.mutations.edits.len(); + self.remove_component_node(left, true); + assert!(self.mutations.edits.len() > pre_edits); + // We want to optimize the replace case to use one less mutation if possible // Since mutations are done in reverse, the last node removed will be the first in the stack // Instead of *just* removing it, we can use the replace mutation @@ -638,6 +668,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() @@ -646,7 +677,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; } @@ -673,7 +704,8 @@ impl<'b> VirtualDom { Component(comp) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(node)) => self.push_all_real_nodes(node), + RenderReturn::Ready(node) => self.push_all_real_nodes(node), + RenderReturn::Aborted(_node) => todo!(), _ => todo!(), } } @@ -701,7 +733,11 @@ impl<'b> VirtualDom { } /// Simply replace a placeholder with a list of nodes - fn replace_placeholder(&mut self, l: &'b VPlaceholder, r: &'b [VNode<'b>]) { + fn replace_placeholder( + &mut self, + l: &'b VPlaceholder, + r: impl IntoIterator>, + ) { let m = self.create_children(r); let id = l.id.get().unwrap(); self.mutations.push(Mutation::ReplaceWith { id, m }); @@ -773,11 +809,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 }); } @@ -790,8 +826,19 @@ 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 { - continue; + 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(len) = path_len { + if (..=1).contains(&len) { + continue; + } } let next_id = attr.mounted_element.get(); @@ -808,12 +855,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); } } @@ -847,14 +898,20 @@ impl<'b> VirtualDom { } fn remove_component_node(&mut self, comp: &VComponent, gen_muts: bool) { - let scope = comp.scope.take().unwrap(); + let scope = comp + .scope + .take() + .expect("VComponents to always have a scope"); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(t)) => { - println!("Removing component node sync {:?}", gen_muts); - self.remove_node(t, gen_muts) + RenderReturn::Ready(t) => self.remove_node(t, gen_muts), + RenderReturn::Aborted(t) => { + if let Some(id) = t.id.get() { + self.try_reclaim(id); + } + return; } - _ => todo!("cannot handle nonstandard nodes"), + _ => todo!(), }; let props = self.scopes[scope.0].props.take(); @@ -873,14 +930,14 @@ 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(), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(t)) => self.find_first_element(t), + RenderReturn::Ready(t) => self.find_first_element(t), _ => todo!("cannot handle nonstandard nodes"), } } @@ -888,15 +945,15 @@ 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(), Some(Component(comp)) => { let scope = comp.scope.get().unwrap(); match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Some(t)) => self.find_last_element(t), + RenderReturn::Ready(t) => self.find_last_element(t), _ => todo!("cannot handle nonstandard nodes"), } } @@ -911,27 +968,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), @@ -956,11 +1016,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/error_boundary.rs b/packages/core/src/error_boundary.rs index fd8810f1a..071fbe48b 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -1,19 +1,203 @@ -use std::cell::RefCell; - -use crate::ScopeId; +use crate::{ScopeId, ScopeState}; +use std::{ + any::{Any, TypeId}, + cell::RefCell, + fmt::Debug, +}; /// A boundary that will capture any errors from child components -#[allow(dead_code)] pub struct ErrorBoundary { - error: RefCell>, - id: ScopeId, + error: RefCell>, + _id: ScopeId, +} + +/// An instance of an error captured by a descendant component. +pub struct CapturedError { + /// The error captured by the error boundary + pub error: Box, + + /// The scope that threw the error + pub scope: ScopeId, +} + +impl CapturedError { + /// Downcast the error type into a concrete error type + pub fn downcast(&self) -> Option<&T> { + if TypeId::of::() == self.error.type_id() { + let raw = self.error.as_ref() as *const _ as *const T; + Some(unsafe { &*raw }) + } else { + None + } + } } impl ErrorBoundary { pub fn new(id: ScopeId) -> Self { Self { error: RefCell::new(None), - id, + _id: id, + } + } + + /// Push an error into this Error Boundary + pub fn insert_error(&self, scope: ScopeId, error: Box) { + self.error.replace(Some(CapturedError { error, scope })); + } +} + +/// A trait to allow results to be thrown upwards to the nearest Error Boundary +/// +/// The canonical way of using this trait is to throw results from hooks, aborting rendering +/// through question mark synax. The throw method returns an option that evalutes to None +/// if there is an error, injecting the error to the nearest error boundary. +/// +/// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess. +/// +/// The call stack is saved for this component and provided to the error boundary +/// +/// ```rust, ignore +/// +/// #[inline_props] +/// fn app(cx: Scope, count: String) -> Element { +/// let id: i32 = count.parse().throw(cx)?; +/// +/// cx.render(rsx! { +/// div { "Count {}" } +/// }) +/// } +/// ``` +pub trait Throw: Sized { + /// The value that will be returned in if the given value is `Ok`. + type Out; + + /// Returns an option that evalutes to None if there is an error, injecting the error to the nearest error boundary. + /// + /// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess. + /// + /// The call stack is saved for this component and provided to the error boundary + /// + /// + /// Note that you can also manually throw errors using the throw method on `ScopeState` directly, + /// which is what this trait shells out to. + /// + /// + /// ```rust, ignore + /// + /// #[inline_props] + /// fn app(cx: Scope, count: String) -> Element { + /// let id: i32 = count.parse().throw(cx)?; + /// + /// cx.render(rsx! { + /// div { "Count {}" } + /// }) + /// } + /// ``` + fn throw(self, cx: &ScopeState) -> Option; + + /// Returns an option that evalutes to None if there is an error, injecting the error to the nearest error boundary. + /// + /// If the value is `Ok`, then throw returns the value, not aborting the rendering preocess. + /// + /// The call stack is saved for this component and provided to the error boundary + /// + /// + /// Note that you can also manually throw errors using the throw method on `ScopeState` directly, + /// which is what this trait shells out to. + /// + /// + /// ```rust, ignore + /// + /// #[inline_props] + /// fn app(cx: Scope, count: String) -> Element { + /// let id: i32 = count.parse().throw(cx)?; + /// + /// cx.render(rsx! { + /// div { "Count {}" } + /// }) + /// } + /// ``` + fn throw_with( + self, + cx: &ScopeState, + e: impl FnOnce() -> D, + ) -> Option; +} + +/// We call clone on any errors that can be owned out of a reference +impl<'a, T, O: Debug + 'static, E: ToOwned> Throw for &'a Result { + type Out = &'a T; + + fn throw(self, cx: &ScopeState) -> Option { + match self { + Ok(t) => Some(t), + Err(e) => { + cx.throw(e.to_owned()); + None + } + } + } + + fn throw_with( + self, + cx: &ScopeState, + err: impl FnOnce() -> D, + ) -> Option { + match self { + Ok(t) => Some(t), + Err(_e) => { + cx.throw(err()); + None + } } } } + +/// Or just throw errors we know about +impl Throw for Result { + type Out = T; + + fn throw(self, cx: &ScopeState) -> Option { + match self { + Ok(t) => Some(t), + Err(e) => { + cx.throw(e); + None + } + } + } + + fn throw_with( + self, + cx: &ScopeState, + error: impl FnOnce() -> D, + ) -> Option { + self.ok().or_else(|| { + cx.throw(error()); + None + }) + } +} + +/// Or just throw errors we know about +impl Throw for Option { + type Out = T; + + fn throw(self, cx: &ScopeState) -> Option { + self.or_else(|| { + cx.throw("None error."); + None + }) + } + + fn throw_with( + self, + cx: &ScopeState, + error: impl FnOnce() -> D, + ) -> Option { + self.or_else(|| { + cx.throw(error()); + None + }) + } +} 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/lib.rs b/packages/core/src/lib.rs index d6ae70c04..f13c20b43 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -70,10 +70,10 @@ pub(crate) mod innerlude { } pub use crate::innerlude::{ - fc_to_builder, AnyValue, AnyValueContainer, Attribute, AttributeValue, Component, DynamicNode, - Element, ElementId, Event, Fragment, IntoAttributeValue, IntoDynNode, LazyNodes, Mutation, - Mutations, Properties, RenderReturn, Scope, ScopeId, ScopeState, Scoped, SuspenseContext, - TaskId, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VText, VirtualDom, + fc_to_builder, Attribute, AttributeValue, CapturedError, Component, DynamicNode, Element, + ElementId, Event, Fragment, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, + RenderReturn, Scope, ScopeId, ScopeState, Scoped, SuspenseContext, TaskId, Template, + TemplateAttribute, TemplateNode, VComponent, VNode, VText, VirtualDom, }; /// The purpose of this module is to alleviate imports of many common types @@ -81,9 +81,9 @@ pub use crate::innerlude::{ /// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ - fc_to_builder, Component, Element, Event, EventHandler, Fragment, IntoAttributeValue, - LazyNodes, Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, - TemplateAttribute, TemplateNode, VNode, VirtualDom, + fc_to_builder, Component, Element, Event, EventHandler, Fragment, LazyNodes, Properties, + Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, + Throw, VNode, VirtualDom, }; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index c0ac4efd4..030e77e8d 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, ops::Deref, @@ -22,10 +22,22 @@ pub type TemplateId = &'static str; /// you might need to handle the case where there's no node immediately ready. pub enum RenderReturn<'a> { /// A currently-available element - Sync(Element<'a>), + Ready(VNode<'a>), + + /// The component aborted rendering early. It might've thrown an error. + /// + /// In its place we've produced a placeholder to locate its spot in the dom when + /// it recovers. + Aborted(VPlaceholder), /// An ongoing future that will resolve to a [`Element`] - Async(BumpBox<'a, dyn Future> + 'a>), + Pending(BumpBox<'a, dyn Future> + 'a>), +} + +impl<'a> Default for RenderReturn<'a> { + fn default() -> Self { + RenderReturn::Aborted(VPlaceholder::default()) + } } /// A reference to a template along with any context needed to hydrate it @@ -43,11 +55,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>], @@ -56,21 +68,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: &[], - }, + }), }) } @@ -78,7 +189,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]) @@ -103,32 +214,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? /// @@ -145,7 +309,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. /// @@ -165,9 +333,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>], }, @@ -544,7 +714,10 @@ pub trait ComponentReturn<'a, A = ()> { impl<'a> ComponentReturn<'a> for Element<'a> { fn into_return(self, _cx: &ScopeState) -> RenderReturn<'a> { - RenderReturn::Sync(self) + match self { + Some(node) => RenderReturn::Ready(node), + None => RenderReturn::default(), + } } } @@ -556,7 +729,7 @@ where { fn into_return(self, cx: &'a ScopeState) -> RenderReturn<'a> { let f: &mut dyn Future> = cx.bump().alloc(self); - RenderReturn::Async(unsafe { BumpBox::from_raw(f) }) + RenderReturn::Pending(unsafe { BumpBox::from_raw(f) }) } } @@ -640,8 +813,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/scheduler/mod.rs b/packages/core/src/scheduler/mod.rs index a6442184d..7aed417b1 100644 --- a/packages/core/src/scheduler/mod.rs +++ b/packages/core/src/scheduler/mod.rs @@ -8,7 +8,7 @@ mod waker; pub use suspense::*; pub use task::*; -pub use waker::RcWake; +pub use waker::ArcWake; /// The type of message that can be sent to the scheduler. /// @@ -25,16 +25,16 @@ pub(crate) enum SchedulerMsg { SuspenseNotified(SuspenseId), } -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, rc::Rc, sync::Arc}; pub(crate) struct Scheduler { pub sender: futures_channel::mpsc::UnboundedSender, /// Tasks created with cx.spawn - pub tasks: RefCell>>, + pub tasks: RefCell>>, /// Async components - pub leaves: RefCell>>, + pub leaves: RefCell>>, } impl Scheduler { diff --git a/packages/core/src/scheduler/suspense.rs b/packages/core/src/scheduler/suspense.rs index 8d7d7337e..c7cd650bb 100644 --- a/packages/core/src/scheduler/suspense.rs +++ b/packages/core/src/scheduler/suspense.rs @@ -1,11 +1,11 @@ -use super::{waker::RcWake, SchedulerMsg}; +use super::{waker::ArcWake, SchedulerMsg}; use crate::ElementId; use crate::{innerlude::Mutations, Element, ScopeId}; use std::future::Future; +use std::sync::Arc; use std::{ cell::{Cell, RefCell}, collections::HashSet, - rc::Rc, }; /// An ID representing an ongoing suspended component @@ -42,8 +42,8 @@ pub(crate) struct SuspenseLeaf { pub(crate) task: *mut dyn Future>, } -impl RcWake for SuspenseLeaf { - fn wake_by_ref(arc_self: &Rc) { +impl ArcWake for SuspenseLeaf { + fn wake_by_ref(arc_self: &Arc) { arc_self.notified.set(true); _ = arc_self .tx diff --git a/packages/core/src/scheduler/task.rs b/packages/core/src/scheduler/task.rs index fb4378104..8c1d7ca76 100644 --- a/packages/core/src/scheduler/task.rs +++ b/packages/core/src/scheduler/task.rs @@ -1,8 +1,9 @@ -use super::{waker::RcWake, Scheduler, SchedulerMsg}; +use super::{waker::ArcWake, Scheduler, SchedulerMsg}; use crate::ScopeId; use std::cell::RefCell; use std::future::Future; -use std::{pin::Pin, rc::Rc}; +use std::pin::Pin; +use std::sync::Arc; /// A task's unique identifier. /// @@ -35,7 +36,7 @@ impl Scheduler { let entry = tasks.vacant_entry(); let task_id = TaskId(entry.key()); - entry.insert(Rc::new(LocalTask { + entry.insert(Arc::new(LocalTask { id: task_id, tx: self.sender.clone(), task: RefCell::new(Box::pin(task)), @@ -57,8 +58,8 @@ impl Scheduler { } } -impl RcWake for LocalTask { - fn wake_by_ref(arc_self: &Rc) { +impl ArcWake for LocalTask { + fn wake_by_ref(arc_self: &Arc) { _ = arc_self .tx .unbounded_send(SchedulerMsg::TaskNotified(arc_self.id)); diff --git a/packages/core/src/scheduler/wait.rs b/packages/core/src/scheduler/wait.rs index 8dff17b01..205bd8ea0 100644 --- a/packages/core/src/scheduler/wait.rs +++ b/packages/core/src/scheduler/wait.rs @@ -10,7 +10,7 @@ use crate::{ ScopeId, TaskId, VNode, VirtualDom, }; -use super::{waker::RcWake, SuspenseId}; +use super::{waker::ArcWake, SuspenseId}; impl VirtualDom { /// Handle notifications by tasks inside the scheduler @@ -67,18 +67,21 @@ impl VirtualDom { // we should attach them to that component and then render its children // continue rendering the tree until we hit yet another suspended component if let Poll::Ready(new_nodes) = as_pinned_mut.poll_unpin(&mut cx) { - // safety: we're not going to modify the suspense context but we don't want to make a clone of it let fiber = self.acquire_suspense_boundary(leaf.scope_id); let scope = &mut self.scopes[scope_id.0]; let arena = scope.current_frame(); - let ret = arena.bump.alloc(RenderReturn::Sync(new_nodes)); + let ret = arena.bump.alloc(match new_nodes { + Some(new) => RenderReturn::Ready(new), + None => RenderReturn::default(), + }); + arena.node.set(ret); fiber.waiting_on.borrow_mut().remove(&id); - if let RenderReturn::Sync(Some(template)) = ret { + if let RenderReturn::Ready(template) = ret { let mutations_ref = &mut fiber.mutations.borrow_mut(); let mutations = &mut **mutations_ref; let template: &VNode = unsafe { std::mem::transmute(template) }; diff --git a/packages/core/src/scheduler/waker.rs b/packages/core/src/scheduler/waker.rs index 762020167..fc75fe8fd 100644 --- a/packages/core/src/scheduler/waker.rs +++ b/packages/core/src/scheduler/waker.rs @@ -1,36 +1,37 @@ +use std::mem; +use std::sync::Arc; use std::task::{RawWaker, RawWakerVTable, Waker}; -use std::{mem, rc::Rc}; -pub trait RcWake: Sized { +pub trait ArcWake: Sized { /// Create a waker from this self-wakening object - fn waker(self: &Rc) -> Waker { - unsafe fn rc_vtable() -> &'static RawWakerVTable { + fn waker(self: &Arc) -> Waker { + unsafe fn rc_vtable() -> &'static RawWakerVTable { &RawWakerVTable::new( |data| { - let arc = mem::ManuallyDrop::new(Rc::::from_raw(data.cast::())); + let arc = mem::ManuallyDrop::new(Arc::::from_raw(data.cast::())); let _rc_clone: mem::ManuallyDrop<_> = arc.clone(); RawWaker::new(data, rc_vtable::()) }, - |data| Rc::from_raw(data.cast::()).wake(), + |data| Arc::from_raw(data.cast::()).wake(), |data| { - let arc = mem::ManuallyDrop::new(Rc::::from_raw(data.cast::())); - RcWake::wake_by_ref(&arc); + let arc = mem::ManuallyDrop::new(Arc::::from_raw(data.cast::())); + ArcWake::wake_by_ref(&arc); }, - |data| drop(Rc::::from_raw(data.cast::())), + |data| drop(Arc::::from_raw(data.cast::())), ) } unsafe { Waker::from_raw(RawWaker::new( - Rc::into_raw(self.clone()).cast(), + Arc::into_raw(self.clone()).cast(), rc_vtable::(), )) } } - fn wake_by_ref(arc_self: &Rc); + fn wake_by_ref(arc_self: &Arc); - fn wake(self: Rc) { + fn wake(self: Arc) { Self::wake_by_ref(&self) } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 6dd5316d3..7cbca96e4 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -4,7 +4,7 @@ use crate::{ innerlude::DirtyScope, innerlude::{SuspenseId, SuspenseLeaf}, nodes::RenderReturn, - scheduler::RcWake, + scheduler::ArcWake, scopes::{ScopeId, ScopeState}, virtual_dom::VirtualDom, }; @@ -13,7 +13,7 @@ use futures_util::FutureExt; use std::{ mem, pin::Pin, - rc::Rc, + sync::Arc, task::{Context, Poll}, }; @@ -79,17 +79,18 @@ impl VirtualDom { // safety: due to how we traverse the tree, we know that the scope is not currently aliased let props: &dyn AnyProps = scope.props.as_ref().unwrap().as_ref(); let props: &dyn AnyProps = mem::transmute(props); + props.render(scope).extend_lifetime() }; // immediately resolve futures that can be resolved - if let RenderReturn::Async(task) = &mut new_nodes { + if let RenderReturn::Pending(task) = &mut new_nodes { let mut leaves = self.scheduler.leaves.borrow_mut(); let entry = leaves.vacant_entry(); let suspense_id = SuspenseId(entry.key()); - let leaf = Rc::new(SuspenseLeaf { + let leaf = Arc::new(SuspenseLeaf { scope_id, task: task.as_mut(), id: suspense_id, @@ -108,7 +109,11 @@ impl VirtualDom { match pinned.poll_unpin(&mut cx) { // If nodes are produced, then set it and we can break Poll::Ready(nodes) => { - new_nodes = RenderReturn::Sync(nodes); + new_nodes = match nodes { + Some(nodes) => RenderReturn::Ready(nodes), + None => RenderReturn::default(), + }; + break; } @@ -150,6 +155,6 @@ impl VirtualDom { }); // rebind the lifetime now that its stored internally - unsafe { mem::transmute(allocated) } + unsafe { allocated.extend_lifetime_ref() } } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index 7520df942..20be78cce 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -4,7 +4,7 @@ use crate::{ arena::ElementId, bump_frame::BumpFrame, innerlude::{DynamicNode, EventHandler, VComponent, VText}, - innerlude::{ListenerCb, Scheduler, SchedulerMsg}, + innerlude::{ErrorBoundary, ListenerCb, Scheduler, SchedulerMsg}, lazynodes::LazyNodes, nodes::{ComponentReturn, IntoAttributeValue, IntoDynNode, RenderReturn}, Attribute, AttributeValue, Element, Event, Properties, TaskId, @@ -14,7 +14,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, - fmt::Arguments, + fmt::{Arguments, Debug}, future::Future, rc::Rc, sync::Arc, @@ -513,6 +513,19 @@ impl<'src> ScopeState { AttributeValue::Listener(ListenerCb(RefCell::new(Some(boxed)))) } + /// Inject an error into the nearest error boundary and quit rendering + /// + /// The error doesn't need to implement Error or any specific traits since the boundary + /// itself will downcast the error into a trait object. + pub fn throw(&self, error: impl Debug + 'static) -> Option<()> { + if let Some(cx) = self.consume_context::>() { + cx.insert_error(self.scope_id(), Box::new(error)); + } + + // Always return none during a throw + None + } + /// Store a value between renders. The foundational hook for all other hooks. /// /// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render). The return value of this closure is stored for the lifetime of the component, and a mutable reference to it is provided on every render as the return value of `use_hook`. diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 9afbe2ce9..890416cd9 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::Ready(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. @@ -477,7 +503,7 @@ impl VirtualDom { pub fn rebuild(&mut self) -> Mutations { match unsafe { self.run_scope(ScopeId(0)).extend_lifetime_ref() } { // Rebuilding implies we append the created elements to the root - RenderReturn::Sync(Some(node)) => { + RenderReturn::Ready(node) => { let m = self.create_scope(ScopeId(0), node); self.mutations.edits.push(Mutation::AppendChildren { id: ElementId(0), @@ -485,8 +511,8 @@ impl VirtualDom { }); } // If an error occurs, we should try to render the default error component and context where the error occured - RenderReturn::Sync(None) => panic!("Cannot catch errors during rebuild"), - RenderReturn::Async(_) => unreachable!("Root scope cannot be an async component"), + RenderReturn::Aborted(_placeholder) => panic!("Cannot catch errors during rebuild"), + RenderReturn::Pending(_) => unreachable!("Root scope cannot be an async component"), } self.finalize() diff --git a/packages/core/tests/error_boundary.rs b/packages/core/tests/error_boundary.rs new file mode 100644 index 000000000..7dbfca343 --- /dev/null +++ b/packages/core/tests/error_boundary.rs @@ -0,0 +1,58 @@ +use dioxus::prelude::*; +use futures_util::Future; + +#[test] +fn catches_panic() { + let mut dom = VirtualDom::new(app); + + let a = dom.rebuild(); + + dbg!(a); +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + h1 { "Title" } + + NoneChild {} + } + }) +} + +fn NoneChild(cx: Scope) -> Element { + None +} + +fn PanicChild(cx: Scope) -> Element { + panic!("Rendering panicked for whatever reason"); + + cx.render(rsx! { + h1 { "It works!" } + }) +} + +fn ThrowChild(cx: Scope) -> Element { + cx.throw(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd"))?; + + let g: i32 = "123123".parse().throw(cx)?; + + cx.render(rsx! { + div {} + }) +} + +fn custom_allocator(cx: Scope) -> Element { + let g = String::new(); + + let p = g.as_str(); + + let g2 = cx.use_hook(|| 123); + // cx.spawn(async move { + + // // + // // println!("Thig is {p}"); + // }); + + None +} 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..337a9485c 100644 --- a/packages/desktop/src/lib.rs +++ b/packages/desktop/src/lib.rs @@ -89,7 +89,7 @@ pub fn launch_cfg(root: Component, config_builder: Config) { /// use dioxus::prelude::*; /// /// fn main() { -/// dioxus_desktop::launch_cfg(app, AppProps { name: "asd" }, |c| c); +/// dioxus_desktop::launch_with_props(app, AppProps { name: "asd" }, |c| c); /// } /// /// struct AppProps { @@ -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/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 /// [`