From 91f1b00517a41acfcd2aff8d22dc57b2b2a3cffe Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sat, 19 Jun 2021 20:34:52 -0400 Subject: [PATCH 01/20] wip: move away from patch machine --- notes/SOLVEDPROBLEMS.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/notes/SOLVEDPROBLEMS.md b/notes/SOLVEDPROBLEMS.md index 1329ab64a..6a00a1802 100644 --- a/notes/SOLVEDPROBLEMS.md +++ b/notes/SOLVEDPROBLEMS.md @@ -438,6 +438,8 @@ abstract the real dom ```rust +struct VirtualDom + trait RealDom { type Node: RealNode; fn get_node(&self, id: u32) -> &Self::Node; @@ -454,7 +456,8 @@ trait RealNode { fn set_attr(&mut self, name, value); fn set_class(&mut self); fn remove_attr(&mut self); - fn downcast_mut(&mut self) -> Option<&mut T>; + // We can't have a generic type in trait objects, so instead we provide the inner as Any + fn raw_node_as_any_mut(&mut self) -> &mut dyn Any; } impl VirtualDom { From 45ee803609bf94404f22d89ab2a20ce0698271ff Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 20 Jun 2021 01:52:32 -0400 Subject: [PATCH 02/20] wip: moving to imperative method of dom --- packages/core/.vscode/spellright.dict | 4 + packages/core/Cargo.toml | 8 +- packages/core/architecture.md | 48 +++- packages/core/src/diff.rs | 376 +++++++++++++++----------- packages/core/src/lib.rs | 5 +- packages/core/src/nodes.rs | 28 +- packages/core/src/virtual_dom.rs | 49 +++- 7 files changed, 321 insertions(+), 197 deletions(-) diff --git a/packages/core/.vscode/spellright.dict b/packages/core/.vscode/spellright.dict index 2e96c2100..c0214aff3 100644 --- a/packages/core/.vscode/spellright.dict +++ b/packages/core/.vscode/spellright.dict @@ -1,2 +1,6 @@ Dodrio VDoms +dom +virtualdom +ns +nohasher diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 57bd0c01d..c7767b52b 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -11,13 +11,13 @@ description = "Core functionality for Dioxus - a concurrent renderer-agnostic Vi [dependencies] # todo: use wast for faster load/compile -dioxus-core-macro = { path = "../core-macro", version = "0.1.1" } +dioxus-core-macro = { path="../core-macro", version="0.1.1" } # Backs scopes and graphs between parent and children -generational-arena = { version = "0.2.8" } +generational-arena = { version="0.2.8" } # Bumpalo backs the VNode creation -bumpalo = { version = "3.6.0", features = ["collections", "boxed"] } +bumpalo = { version="3.6.0", features=["collections", "boxed"] } # custom error type thiserror = "1" @@ -32,7 +32,7 @@ longest-increasing-subsequence = "0.1.0" log = "0.4" # Serialize the Edits for use in Webview/Liveview instances -serde = { version = "1", features = ["derive"], optional = true } +serde = { version="1", features=["derive"], optional=true } [features] default = [] diff --git a/packages/core/architecture.md b/packages/core/architecture.md index 9124fe788..47cec84f1 100644 --- a/packages/core/architecture.md +++ b/packages/core/architecture.md @@ -1,23 +1,57 @@ # This module includes all life-cycle related mechanics, including the virtual DOM, scopes, properties, and lifecycles. + --- + The VirtualDom is designed as so: VDOM contains: + - An arena of component scopes. - - A scope contains - - lifecycle data - - hook data + - A scope contains + - lifecycle data + - hook data - Event queue - - An event + - An event A VDOM is + - constructed from anything that implements "component" A "Component" is anything (normally functions) that can be ran with a context to produce VNodes + - Must implement properties-builder trait which produces a properties builder A Context + - Is a consumable struct - - Made of references to properties - - Holds a reference (lockable) to the underlying scope - - Is partially thread-safe + - Made of references to properties + - Holds a reference (lockable) to the underlying scope + - Is partially thread-safe + +# How to interact with the real dom? + +## idea: use only u32 + +pros: + +- allows for 4,294,967,295 nodes (enough) +- u32 is relatively small +- doesn't add type noise +- allows virtualdom to stay completely generic + +cons: + +- cost of querying individual nodes (about 7ns per node query for all sizes w/ nohasher) +- old IDs need to be manually freed when subtrees are destroyed + - can be collected as garbage after every render +- loss of ids between renders........................ + - each new render doesn't know which node the old one was connected to unless it is visited + - When are nodes _not_ visited during diffing? + - They are predetermined to be removed (a parent was probed) + - something with keys? + - I think all nodes must be visited between diffs + - + +## idea: leak raw nodes and then reclaim them on drop + +## idea: bind diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 5b9db7596..2f09fada6 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -9,15 +9,10 @@ //! Implementation Details: //! ----------------------- //! -//! Diff the `old` node with the `new` node. Emits instructions to modify a -//! physical DOM node that reflects `old` into something that reflects `new`. +//! All nodes are addressed by their IDs. The RealDom provides an imperative interface for making changes to these nodes. +//! We don't necessarily intend for changes to happen exactly during the diffing process, so the implementor may choose +//! to batch nodes if it is more performant for their application. The u32 should be a no-op to hash, //! -//! Upon entry to this function, the physical DOM node must be on the top of the -//! change list stack: -//! -//! [... node] -//! -//! The change list stack is in the same state when this function exits. //! //! Further Reading and Thoughts //! ---------------------------- @@ -30,11 +25,58 @@ use crate::{arena::ScopeArena, innerlude::*}; use fxhash::{FxHashMap, FxHashSet}; use std::{ + any::Any, + cell::Cell, cmp::Ordering, rc::{Rc, Weak}, - sync::atomic::AtomicU32, }; +/// The accompanying "real dom" exposes an imperative API for controlling the UI layout +/// +/// Instead of having handles directly over nodes, Dioxus uses simple u32s as node IDs. +/// This allows layouts with up to 4,294,967,295 nodes. If we +pub trait RealDom { + fn delete_root(&mut self, root: RealDomNode); + + // =========== + // Create + // =========== + /// Create a new text node and push it on to the top of the stack + fn create_text_node(&mut self, text: &str) -> RealDomNode; + + /// Create a new text node and push it on to the top of the stack + fn create_element(&mut self, tag: &str) -> RealDomNode; + + /// Create a new namespaced element and push it on to the top of the stack + fn create_element_ns(&mut self, tag: &str, namespace: &str) -> RealDomNode; + + fn append_node(&self, child: RealDomNode, parent: RealDomNode); + + // =========== + // Remove + // =========== + fn remove_node(&mut self, node: RealDomNode); + + fn remove_all_children(&mut self, node: RealDomNode); + + // =========== + // Replace + // =========== + fn replace_node_with(&mut self, old: RealDomNode, new: RealDomNode); + + fn new_event_listener(&mut self, node: RealDomNode, event: &str); + + fn set_inner_text(&mut self, node: RealDomNode, text: &str); + + fn set_class(&mut self, node: RealDomNode); + + fn set_attr(&mut self, node: RealDomNode, name: &str, value: &str); + + fn remove_attr(&mut self, node: RealDomNode); + + fn raw_node_as_any_mut(&mut self) -> &mut dyn Any; +} + /// The DiffState is a cursor internal to the VirtualDOM's diffing algorithm that allows persistence of state while /// diffing trees of components. This means we can "re-enter" a subtree of a component by queuing a "NeedToDiff" event. /// @@ -47,39 +89,39 @@ use std::{ /// The order of these re-entrances is stored in the DiffState itself. The DiffState comes pre-loaded with a set of components /// that were modified by the eventtrigger. This prevents doubly evaluating components if they were both updated via /// subscriptions and props changes. -pub struct DiffMachine<'a> { - pub create_diffs: bool, +pub struct DiffMachine { pub cur_idx: ScopeIdx, - pub change_list: EditMachine<'a>, pub diffed: FxHashSet, pub components: ScopeArena, pub event_queue: EventQueue, pub seen_nodes: FxHashSet, } -static COUNTER: AtomicU32 = AtomicU32::new(1); +// todo: see if unsafe works better +static COUNTER: Cell = Cell::new(1); fn next_id() -> u32 { - COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed) + let out = COUNTER.get(); + COUNTER.set(out + 1); + out } -impl<'a> DiffMachine<'a> { +impl DiffMachine { pub fn new(components: ScopeArena, cur_idx: ScopeIdx, event_queue: EventQueue) -> Self { Self { components, cur_idx, event_queue, - create_diffs: true, - change_list: EditMachine::new(), diffed: FxHashSet::default(), seen_nodes: FxHashSet::default(), } } - pub fn consume(self) -> EditList<'a> { - self.change_list.emitter - } - - pub fn diff_node(&mut self, old_node: &VNode<'a>, new_node: &VNode<'a>) { + pub fn diff_node<'a, Dom: RealDom>( + &mut self, + dom: &mut Dom, + old_node: &mut VNode<'a>, + new_node: &mut VNode<'a>, + ) { // pub fn diff_node(&mut self, old: &VNode<'a>, new: &VNode<'a>) { /* For each valid case, we "commit traversal", meaning we save this current position in the tree. @@ -87,61 +129,64 @@ impl<'a> DiffMachine<'a> { When re-entering, we reuse the EditList in DiffState */ match (old_node, new_node) { - (VNode::Text(old_text), VNode::Text(new_text)) => { - if old_text != new_text { - self.change_list.commit_traversal(); - self.change_list.set_text(new_text); + (VNode::Text(old), VNode::Text(new)) => { + new.dom_id = old.dom_id; + if old.text != new.text { + self.realdom.set_inner_text(new.dom_id, new.text); } } - (VNode::Text(_), VNode::Element(_)) => { - self.change_list.commit_traversal(); + (VNode::Text(old), VNode::Element(new)) => { + // // self.realdom.commit_traversal(); self.create(new_node); - self.change_list.replace_with(); + self.realdom.replace_node_with(old.dom_id, old.dom_id); + // self.realdom.replace_with(); } - (VNode::Element(_), VNode::Text(_)) => { - self.change_list.commit_traversal(); + (VNode::Element(old), VNode::Text(new)) => { + // // self.realdom.commit_traversal(); self.create(new_node); - self.change_list.replace_with(); + self.realdom.replace_node_with(old.dom_id, new.dom_id); + // self.realdom.replace_with(); } - (VNode::Element(eold), VNode::Element(enew)) => { + (VNode::Element(old), VNode::Element(new)) => { // If the element type is completely different, the element needs to be re-rendered completely - if enew.tag_name != eold.tag_name || enew.namespace != eold.namespace { - self.change_list.commit_traversal(); - self.change_list.replace_with(); + if new.tag_name != old.tag_name || new.namespace != old.namespace { + // // self.realdom.commit_traversal(); + // self.realdom.replace_with(); + self.realdom.replace_node_with(old.dom_id, new.dom_id); return; } - self.diff_listeners(eold.listeners, enew.listeners); - self.diff_attr(eold.attributes, enew.attributes, enew.namespace.is_some()); - self.diff_children(eold.children, enew.children); + self.diff_listeners(old.listeners, new.listeners); + self.diff_attr(old.attributes, new.attributes, new.namespace.is_some()); + self.diff_children(old.children, new.children); } - (VNode::Component(cold), VNode::Component(cnew)) => { + (VNode::Component(old), VNode::Component(new)) => { // Make sure we're dealing with the same component (by function pointer) - if cold.user_fc == cnew.user_fc { + if old.user_fc == new.user_fc { // Make sure the new component vnode is referencing the right scope id - let scope_id = cold.ass_scope.borrow().clone(); - *cnew.ass_scope.borrow_mut() = scope_id; + let scope_id = old.ass_scope.borrow().clone(); + *new.ass_scope.borrow_mut() = scope_id; // make sure the component's caller function is up to date self.components .with_scope(scope_id.unwrap(), |scope| { - scope.caller = Rc::downgrade(&cnew.caller) + scope.caller = Rc::downgrade(&new.caller) }) .unwrap(); // React doesn't automatically memoize, but we do. // The cost is low enough to make it worth checking - let should_render = match cold.comparator { - Some(comparator) => comparator(cnew), + let should_render = match old.comparator { + Some(comparator) => comparator(new), None => true, }; if should_render { - self.change_list.commit_traversal(); + // // self.realdom.commit_traversal(); self.components .with_scope(scope_id.unwrap(), |f| { f.run_scope().unwrap(); @@ -159,28 +204,29 @@ impl<'a> DiffMachine<'a> { // A new component has shown up! We need to destroy the old node // Wipe the old one and plant the new one - self.change_list.commit_traversal(); + // self.realdom.commit_traversal(); self.create(new_node); - self.change_list.replace_with(); + // self.realdom.replace_node_with(old.dom_id, new.dom_id); + self.realdom.replace_with(); // Now we need to remove the old scope and all of its descendents - let old_scope = cold.ass_scope.borrow().as_ref().unwrap().clone(); + let old_scope = old.ass_scope.borrow().as_ref().unwrap().clone(); self.destroy_scopes(old_scope); } } // todo: knock out any listeners (_, VNode::Component(_)) => { - self.change_list.commit_traversal(); + // self.realdom.commit_traversal(); self.create(new_node); - self.change_list.replace_with(); + self.realdom.replace_with(); } // A component is being torn down in favor of a non-component node (VNode::Component(_old), _) => { - self.change_list.commit_traversal(); + // self.realdom.commit_traversal(); self.create(new_node); - self.change_list.replace_with(); + self.realdom.replace_with(); // Destroy the original scope and any of its children self.destroy_scopes(_old.ass_scope.borrow().unwrap()); @@ -223,35 +269,46 @@ impl<'a> DiffMachine<'a> { // When this function returns, the new node is on top of the change list stack: // // [... node] - fn create(&mut self, node: &VNode<'a>) { - debug_assert!(self.change_list.traversal_is_committed()); + fn create<'a, Dom: RealDom>( + &mut self, + dom: &mut Dom, + node: &mut VNode<'a>, + parent: RealDomNode, + ) { + // debug_assert!(self.realdom.traversal_is_committed()); match node { VNode::Text(text) => { - self.change_list.create_text_node(text); + let real_id = self.realdom.create_text_node(text.text); + text.dom_id = real_id; } - VNode::Element(&VElement { - key, - tag_name, - listeners, - attributes, - children, - namespace, - }) => { + VNode::Element(&el) => { + let VElement { + key, + tag_name, + listeners, + attributes, + children, + namespace, + dom_id, + } = el; // log::info!("Creating {:#?}", node); - if let Some(namespace) = namespace { - self.change_list.create_element_ns(tag_name, namespace); + let real_id = if let Some(namespace) = namespace { + self.realdom.create_element_ns(tag_name, namespace) } else { - self.change_list.create_element(tag_name); - } + self.realdom.create_element(tag_name) + }; + el.dom_id = real_id; listeners.iter().enumerate().for_each(|(_id, listener)| { - self.change_list - .new_event_listener(listener.event, listener.scope, listener.id) + todo!() + // self.realdom + // .new_event_listener(listener.event, listener.scope, listener.id) }); for attr in attributes { - self.change_list - .set_attribute(&attr.name, &attr.value, namespace.is_some()); + todo!() + // self.realdom + // .set_attribute(&attr.name, &attr.value, namespace.is_some()); } // Fast path: if there is a single text child, it is faster to @@ -262,28 +319,27 @@ impl<'a> DiffMachine<'a> { // parent. if children.len() == 1 { if let VNode::Text(text) = children[0] { - self.change_list.set_text(text); + self.realdom.set_inner_text(real_id, text.text); return; } } for child in children { - self.create(child); + self.create(child, real_id); if let VNode::Fragment(_) = child { // do nothing // fragments append themselves } else { - self.change_list.append_child(); + self.realdom.append_child(); } } } VNode::Component(component) => { - self.change_list - .create_text_node("placeholder for vcomponent"); + self.realdom.create_text_node("placeholder for vcomponent"); - let root_id = next_id(); - self.change_list.save_known_root(root_id); + // let root_id = next_id(); + // self.realdom.save_known_root(root_id); log::debug!("Mounting a new component"); let caller: Weak = Rc::downgrade(&component.caller); @@ -333,7 +389,8 @@ impl<'a> DiffMachine<'a> { new_component.run_scope().unwrap(); // And then run the diff algorithm - self.diff_node(new_component.old_frame(), new_component.next_frame()); + todo!(); + // self.diff_node(new_component.old_frame(), new_component.next_frame()); // Finally, insert this node as a seen node. self.seen_nodes.insert(idx); @@ -343,8 +400,9 @@ impl<'a> DiffMachine<'a> { VNode::Fragment(frag) => { // create the children directly in the space for child in frag.children { - self.create(child); - self.change_list.append_child(); + todo!() + // self.create(child); + // self.realdom.append_child(); } } @@ -395,7 +453,7 @@ impl<'a> DiffMachine<'a> { // The change list stack is left unchanged. fn diff_listeners(&mut self, old: &[Listener<'_>], new: &[Listener<'_>]) { if !old.is_empty() || !new.is_empty() { - self.change_list.commit_traversal(); + // self.realdom.commit_traversal(); } 'outer1: for (_l_idx, new_l) in new.iter().enumerate() { @@ -410,8 +468,8 @@ impl<'a> DiffMachine<'a> { for old_l in old { if new_l.event == old_l.event { if new_l.id != old_l.id { - self.change_list.remove_event_listener(event_type); - self.change_list + self.realdom.remove_event_listener(event_type); + self.realdom .update_event_listener(event_type, new_l.scope, new_l.id) } @@ -419,7 +477,7 @@ impl<'a> DiffMachine<'a> { } } - self.change_list + self.realdom .new_event_listener(event_type, new_l.scope, new_l.id); } @@ -429,7 +487,7 @@ impl<'a> DiffMachine<'a> { continue 'outer2; } } - self.change_list.remove_event_listener(old_l.event); + self.realdom.remove_event_listener(old_l.event); } } @@ -440,7 +498,7 @@ impl<'a> DiffMachine<'a> { // [... node] // // The change list stack is left unchanged. - fn diff_attr( + fn diff_attr<'a>( &mut self, old: &'a [Attribute<'a>], new: &'a [Attribute<'a>], @@ -453,15 +511,15 @@ impl<'a> DiffMachine<'a> { // With the Rsx and Html macros, this will almost always be the case 'outer: for new_attr in new { if new_attr.is_volatile() { - self.change_list.commit_traversal(); - self.change_list + // self.realdom.commit_traversal(); + self.realdom .set_attribute(new_attr.name, new_attr.value, is_namespaced); } else { for old_attr in old { if old_attr.name == new_attr.name { if old_attr.value != new_attr.value { - self.change_list.commit_traversal(); - self.change_list.set_attribute( + // self.realdom.commit_traversal(); + self.realdom.set_attribute( new_attr.name, new_attr.value, is_namespaced, @@ -473,8 +531,8 @@ impl<'a> DiffMachine<'a> { } } - self.change_list.commit_traversal(); - self.change_list + // self.realdom.commit_traversal(); + self.realdom .set_attribute(new_attr.name, new_attr.value, is_namespaced); } } @@ -486,8 +544,8 @@ impl<'a> DiffMachine<'a> { } } - self.change_list.commit_traversal(); - self.change_list.remove_attribute(old_attr.name); + // self.realdom.commit_traversal(); + self.realdom.remove_attribute(old_attr.name); } } @@ -499,10 +557,10 @@ impl<'a> DiffMachine<'a> { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_children(&mut self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { + fn diff_children<'a>(&mut self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { if new.is_empty() { if !old.is_empty() { - self.change_list.commit_traversal(); + // self.realdom.commit_traversal(); self.remove_all_children(old); } return; @@ -515,8 +573,8 @@ impl<'a> DiffMachine<'a> { } (_, &VNode::Text(text)) => { - self.change_list.commit_traversal(); - self.change_list.set_text(text); + // self.realdom.commit_traversal(); + self.realdom.set_text(text); return; } @@ -527,7 +585,7 @@ impl<'a> DiffMachine<'a> { if old.is_empty() { if !new.is_empty() { - self.change_list.commit_traversal(); + // self.realdom.commit_traversal(); self.create_and_append_children(new); } return; @@ -546,9 +604,9 @@ impl<'a> DiffMachine<'a> { ); if new_is_keyed && old_is_keyed { - let t = self.change_list.next_temporary(); + let t = self.realdom.next_temporary(); self.diff_keyed_children(old, new); - self.change_list.set_next_temporary(t); + self.realdom.set_next_temporary(t); } else { self.diff_non_keyed_children(old, new); } @@ -575,7 +633,7 @@ impl<'a> DiffMachine<'a> { // [... parent] // // Upon exiting, the change list stack is in the same state. - fn diff_keyed_children(&mut self, old: &[VNode<'a>], new: &[VNode<'a>]) { + fn diff_keyed_children<'a>(&mut self, old: &[VNode<'a>], new: &[VNode<'a>]) { // if cfg!(debug_assertions) { // let mut keys = fxhash::FxHashSet::default(); // let mut assert_unique_keys = |children: &[VNode]| { @@ -658,8 +716,8 @@ impl<'a> DiffMachine<'a> { // [... parent] // // Upon exit, the change list stack is the same. - fn diff_keyed_prefix(&mut self, old: &[VNode<'a>], new: &[VNode<'a>]) -> KeyedPrefixResult { - self.change_list.go_down(); + fn diff_keyed_prefix<'a>(&mut self, old: &[VNode<'a>], new: &[VNode<'a>]) -> KeyedPrefixResult { + // self.realdom.go_down(); let mut shared_prefix_count = 0; for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() { @@ -667,7 +725,7 @@ impl<'a> DiffMachine<'a> { break; } - self.change_list.go_to_sibling(i); + self.realdom.go_to_sibling(i); self.diff_node(old, new); @@ -677,8 +735,8 @@ impl<'a> DiffMachine<'a> { // If that was all of the old children, then create and append the remaining // new children and we're finished. if shared_prefix_count == old.len() { - self.change_list.go_up(); - self.change_list.commit_traversal(); + self.realdom.go_up(); + // self.realdom.commit_traversal(); self.create_and_append_children(&new[shared_prefix_count..]); return KeyedPrefixResult::Finished; } @@ -686,13 +744,13 @@ impl<'a> DiffMachine<'a> { // And if that was all of the new children, then remove all of the remaining // old children and we're finished. if shared_prefix_count == new.len() { - self.change_list.go_to_sibling(shared_prefix_count); - self.change_list.commit_traversal(); + self.realdom.go_to_sibling(shared_prefix_count); + // self.realdom.commit_traversal(); self.remove_self_and_next_siblings(&old[shared_prefix_count..]); return KeyedPrefixResult::Finished; } - self.change_list.go_up(); + self.realdom.go_up(); KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) } @@ -709,7 +767,7 @@ impl<'a> DiffMachine<'a> { // [... parent] // // Upon exit from this function, it will be restored to that same state. - fn diff_keyed_middle( + fn diff_keyed_middle<'a>( &mut self, old: &[VNode<'a>], mut new: &[VNode<'a>], @@ -753,11 +811,11 @@ impl<'a> DiffMachine<'a> { // afresh. if shared_suffix_count == 0 && shared_keys.is_empty() { if shared_prefix_count == 0 { - self.change_list.commit_traversal(); + // self.realdom.commit_traversal(); self.remove_all_children(old); } else { - self.change_list.go_down_to_child(shared_prefix_count); - self.change_list.commit_traversal(); + self.realdom.go_down_to_child(shared_prefix_count); + // self.realdom.commit_traversal(); self.remove_self_and_next_siblings(&old[shared_prefix_count..]); } @@ -779,8 +837,8 @@ impl<'a> DiffMachine<'a> { .unwrap_or(old.len()); if end - start > 0 { - self.change_list.commit_traversal(); - let mut t = self.change_list.save_children_to_temporaries( + // self.realdom.commit_traversal(); + let mut t = self.realdom.save_children_to_temporaries( shared_prefix_count + start, shared_prefix_count + end, ); @@ -805,8 +863,8 @@ impl<'a> DiffMachine<'a> { if !shared_keys.contains(&old_child.key()) { // registry.remove_subtree(old_child); // todo - self.change_list.commit_traversal(); - self.change_list.remove_child(i + shared_prefix_count); + // self.realdom.commit_traversal(); + self.realdom.remove_child(i + shared_prefix_count); removed_count += 1; } } @@ -849,7 +907,7 @@ impl<'a> DiffMachine<'a> { // shared suffix to the change list stack. // // [... parent] - self.change_list + self.realdom .go_down_to_child(old_shared_suffix_start - removed_count); // [... parent first_child_of_shared_suffix] } else { @@ -865,29 +923,29 @@ impl<'a> DiffMachine<'a> { let old_index = new_index_to_old_index[last_index]; let temp = old_index_to_temp[old_index]; // [... parent] - self.change_list.go_down_to_temp_child(temp); + self.realdom.go_down_to_temp_child(temp); // [... parent last] self.diff_node(&old[old_index], last); if new_index_is_in_lis.contains(&last_index) { // Don't move it, since it is already where it needs to be. } else { - self.change_list.commit_traversal(); + // self.realdom.commit_traversal(); // [... parent last] - self.change_list.append_child(); + self.realdom.append_child(); // [... parent] - self.change_list.go_down_to_temp_child(temp); + self.realdom.go_down_to_temp_child(temp); // [... parent last] } } else { - self.change_list.commit_traversal(); + // self.realdom.commit_traversal(); // [... parent] self.create(last); // [... parent last] - self.change_list.append_child(); + self.realdom.append_child(); // [... parent] - self.change_list.go_down_to_reverse_child(0); + self.realdom.go_down_to_reverse_child(0); // [... parent last] } } @@ -896,11 +954,11 @@ impl<'a> DiffMachine<'a> { let old_index = new_index_to_old_index[new_index]; if old_index == u32::MAX as usize { debug_assert!(!shared_keys.contains(&new_child.key())); - self.change_list.commit_traversal(); + // self.realdom.commit_traversal(); // [... parent successor] self.create(new_child); // [... parent successor new_child] - self.change_list.insert_before(); + self.realdom.insert_before(); // [... parent new_child] } else { debug_assert!(shared_keys.contains(&new_child.key())); @@ -909,14 +967,14 @@ impl<'a> DiffMachine<'a> { if new_index_is_in_lis.contains(&new_index) { // [... parent successor] - self.change_list.go_to_temp_sibling(temp); + self.realdom.go_to_temp_sibling(temp); // [... parent new_child] } else { - self.change_list.commit_traversal(); + // self.realdom.commit_traversal(); // [... parent successor] - self.change_list.push_temporary(temp); + self.realdom.push_temporary(temp); // [... parent successor new_child] - self.change_list.insert_before(); + self.realdom.insert_before(); // [... parent new_child] } @@ -925,7 +983,7 @@ impl<'a> DiffMachine<'a> { } // [... parent child] - self.change_list.go_up(); + self.realdom.go_up(); // [... parent] } @@ -936,7 +994,7 @@ impl<'a> DiffMachine<'a> { // [... parent] // // When this function exits, the change list stack remains the same. - fn diff_keyed_suffix( + fn diff_keyed_suffix<'a>( &mut self, old: &[VNode<'a>], new: &[VNode<'a>], @@ -946,16 +1004,16 @@ impl<'a> DiffMachine<'a> { debug_assert!(!old.is_empty()); // [... parent] - self.change_list.go_down(); + self.realdom.go_down(); // [... parent new_child] for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() { - self.change_list.go_to_sibling(new_shared_suffix_start + i); + self.realdom.go_to_sibling(new_shared_suffix_start + i); self.diff_node(old_child, new_child); } // [... parent] - self.change_list.go_up(); + self.realdom.go_up(); } // Diff children that are not keyed. @@ -966,18 +1024,18 @@ impl<'a> DiffMachine<'a> { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children(&mut self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { + fn diff_non_keyed_children<'a>(&mut self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { // Handled these cases in `diff_children` before calling this function. debug_assert!(!new.is_empty()); debug_assert!(!old.is_empty()); // [... parent] - self.change_list.go_down(); + self.realdom.go_down(); // [... parent child] for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() { // [... parent prev_child] - self.change_list.go_to_sibling(i); + self.realdom.go_to_sibling(i); // [... parent this_child] self.diff_node(old_child, new_child); } @@ -986,9 +1044,9 @@ impl<'a> DiffMachine<'a> { // old.len > new.len -> removing some nodes Ordering::Greater => { // [... parent prev_child] - self.change_list.go_to_sibling(new.len()); + self.realdom.go_to_sibling(new.len()); // [... parent first_child_to_remove] - self.change_list.commit_traversal(); + // self.realdom.commit_traversal(); // support::remove_self_and_next_siblings(state, &old[new.len()..]); self.remove_self_and_next_siblings(&old[new.len()..]); // [... parent] @@ -996,15 +1054,15 @@ impl<'a> DiffMachine<'a> { // old.len < new.len -> adding some nodes Ordering::Less => { // [... parent last_child] - self.change_list.go_up(); + self.realdom.go_up(); // [... parent] - self.change_list.commit_traversal(); + // self.realdom.commit_traversal(); self.create_and_append_children(&new[old.len()..]); } // old.len == new.len -> no nodes added/removed, but πerhaps changed Ordering::Equal => { // [... parent child] - self.change_list.go_up(); + self.realdom.go_up(); // [... parent] } } @@ -1021,15 +1079,15 @@ impl<'a> DiffMachine<'a> { // [... parent] // // When this function returns, the change list stack is in the same state. - pub fn remove_all_children(&mut self, old: &[VNode<'a>]) { - debug_assert!(self.change_list.traversal_is_committed()); + pub fn remove_all_children<'a>(&mut self, old: &[VNode<'a>]) { + // debug_assert!(self.realdom.traversal_is_committed()); log::debug!("REMOVING CHILDREN"); for _child in old { // registry.remove_subtree(child); } // Fast way to remove all children: set the node's textContent to an empty // string. - self.change_list.set_text(""); + self.realdom.set_text(""); } // Create the given children and append them to the parent node. @@ -1039,11 +1097,11 @@ impl<'a> DiffMachine<'a> { // [... parent] // // When this function returns, the change list stack is in the same state. - pub fn create_and_append_children(&mut self, new: &[VNode<'a>]) { - debug_assert!(self.change_list.traversal_is_committed()); + pub fn create_and_append_children<'a>(&mut self, new: &[VNode<'a>]) { + // debug_assert!(self.realdom.traversal_is_committed()); for child in new { self.create(child); - self.change_list.append_child(); + self.realdom.append_child(); } } @@ -1056,11 +1114,11 @@ impl<'a> DiffMachine<'a> { // After the function returns, the child is no longer on the change list stack: // // [... parent] - pub fn remove_self_and_next_siblings(&mut self, old: &[VNode<'a>]) { - debug_assert!(self.change_list.traversal_is_committed()); + pub fn remove_self_and_next_siblings<'a>(&mut self, old: &[VNode<'a>]) { + // debug_assert!(self.realdom.traversal_is_committed()); for child in old { if let VNode::Component(vcomp) = child { - // self.change_list + // self.realdom // .create_text_node("placeholder for vcomponent"); todo!() @@ -1071,7 +1129,7 @@ impl<'a> DiffMachine<'a> { // }) // let id = get_id(); // *component.stable_addr.as_ref().borrow_mut() = Some(id); - // self.change_list.save_known_root(id); + // self.realdom.save_known_root(id); // let scope = Rc::downgrade(&component.ass_scope); // self.lifecycle_events.push_back(LifeCycleEvent::Mount { // caller: Rc::downgrade(&component.caller), @@ -1082,7 +1140,7 @@ impl<'a> DiffMachine<'a> { // registry.remove_subtree(child); } - self.change_list.remove_self_and_next_siblings(); + self.realdom.remove_self_and_next_siblings(); } } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index c7cf5d2a3..3f735d9a6 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -11,7 +11,7 @@ pub mod arena; pub mod component; // Logic for extending FC -pub mod debug_renderer; +// pub mod debug_renderer; pub mod diff; pub mod patch; // An "edit phase" described by transitions and edit operations // Test harness for validating that lifecycles and diffs work appropriately // the diffing algorithm that builds the ChangeList @@ -30,7 +30,6 @@ pub mod builder { pub(crate) mod innerlude { pub use crate::component::*; - pub use crate::debug_renderer::*; pub use crate::diff::*; pub use crate::error::*; pub use crate::events::*; @@ -77,6 +76,6 @@ pub mod prelude { pub use crate::diff::DiffMachine; pub use crate::virtual_dom::ScopeIdx; - pub use crate::debug_renderer::DebugRenderer; + // pub use crate::debug_renderer::DebugRenderer; pub use crate::hooks::*; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index dde21a7f7..17f1ba9f1 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -7,13 +7,12 @@ use crate::{ events::VirtualEvent, innerlude::{Context, Properties, Scope, ScopeIdx, FC}, nodebuilder::text3, - virtual_dom::NodeCtx, - // support::NodeCtx, + virtual_dom::{NodeCtx, RealDomNode}, }; use bumpalo::Bump; use std::{ any::Any, - cell::RefCell, + cell::{Cell, RefCell}, fmt::{Arguments, Debug}, marker::PhantomData, rc::Rc, @@ -29,7 +28,7 @@ pub enum VNode<'src> { Element(&'src VElement<'src>), /// A text node (node type `TEXT_NODE`). - Text(&'src str), + Text(VText<'src>), /// A fragment is a "virtual position" in the DOM /// Fragments may have children and keys @@ -49,7 +48,7 @@ impl<'a> Clone for VNode<'a> { fn clone(&self) -> Self { match self { VNode::Element(element) => VNode::Element(element), - VNode::Text(text) => VNode::Text(text), + VNode::Text(old) => VNode::Text(old.clone()), VNode::Fragment(fragment) => VNode::Fragment(fragment), VNode::Component(component) => VNode::Component(component), VNode::Suspended => VNode::Suspended, @@ -81,6 +80,7 @@ impl<'a> VNode<'a> { attributes, children, namespace, + dom_id: Cell::new(RealDomNode::empty()), }); VNode::Element(element) } @@ -88,7 +88,10 @@ impl<'a> VNode<'a> { /// Construct a new text node with the given text. #[inline] pub fn text(text: &'a str) -> VNode<'a> { - VNode::Text(text) + VNode::Text(VText { + text, + dom_id: Cell::new(RealDomNode::empty()), + }) } pub fn text_args(bump: &'a Bump, args: Arguments) -> VNode<'a> { @@ -98,7 +101,7 @@ impl<'a> VNode<'a> { #[inline] pub(crate) fn key(&self) -> NodeKey { match &self { - VNode::Text(_) => NodeKey::NONE, + VNode::Text { .. } => NodeKey::NONE, VNode::Element(e) => e.key, VNode::Fragment(frag) => frag.key, VNode::Component(c) => c.key, @@ -109,6 +112,12 @@ impl<'a> VNode<'a> { } } +#[derive(Clone)] +pub struct VText<'src> { + pub text: &'src str, + pub dom_id: Cell, +} + // ======================================================== // VElement (div, h1, etc), attrs, keys, listener handle // ======================================================== @@ -120,6 +129,7 @@ pub struct VElement<'a> { pub attributes: &'a [Attribute<'a>], pub children: &'a [VNode<'a>], pub namespace: Option<&'a str>, + pub dom_id: Cell, } /// An attribute on a DOM node, such as `id="my-thing"` or @@ -224,7 +234,7 @@ pub type VCompAssociatedScope = Option; pub struct VComponent<'src> { pub key: NodeKey<'src>, - pub stable_addr: RefCell, + pub mounted_root: RealDomNode, pub ass_scope: RefCell, // pub comparator: Rc bool + 'src>, @@ -325,7 +335,7 @@ impl<'a> VComponent<'a> { raw_props, children, caller, - stable_addr: RefCell::new(None), + mounted_root: RealDomNode::empty(), } } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 2c04a99d1..c65f3d84a 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -63,6 +63,14 @@ pub struct VirtualDom { _root_prop_type: std::any::TypeId, } +#[derive(Clone, Copy)] +pub struct RealDomNode(u32); +impl RealDomNode { + pub fn empty() -> Self { + Self(u32::MIN) + } +} + // ====================================== // Public Methods for the VirtualDom // ====================================== @@ -174,10 +182,15 @@ impl VirtualDom { _root_prop_type: TypeId::of::

(), } } +} +// ====================================== +// Private Methods for the VirtualDom +// ====================================== +impl VirtualDom { /// Performs a *full* rebuild of the virtual dom, returning every edit required to generate the actual dom rom scratch /// Currently this doesn't do what we want it to do - pub fn rebuild<'s>(&'s mut self) -> Result> { + pub fn rebuild<'s, Dom: RealDom>(&'s mut self, realdom: &mut Dom) -> Result<()> { let mut diff_machine = DiffMachine::new( self.components.clone(), self.base_scope, @@ -193,16 +206,10 @@ impl VirtualDom { let update = &base.event_channel; update(); - self.progress_completely(&mut diff_machine)?; + self.progress_completely(realdom, &mut diff_machine)?; - Ok(diff_machine.consume()) + Ok(()) } -} - -// ====================================== -// Private Methods for the VirtualDom -// ====================================== -impl VirtualDom { /// This method is the most sophisticated way of updating the virtual dom after an external event has been triggered. /// /// Given a synthetic event, the component that triggered the event, and the index of the callback, this runs the virtual @@ -246,7 +253,11 @@ impl VirtualDom { // but the guarantees provide a safe, fast, and efficient abstraction for the VirtualDOM updating framework. // // A good project would be to remove all unsafe from this crate and move the unsafety into safer abstractions. - pub fn progress_with_event(&mut self, event: EventTrigger) -> Result { + pub fn progress_with_event( + &mut self, + realdom: &mut Dom, + event: EventTrigger, + ) -> Result<()> { let id = event.component_id.clone(); self.components.try_get_mut(id)?.call_listener(event)?; @@ -254,18 +265,19 @@ impl VirtualDom { let mut diff_machine = DiffMachine::new(self.components.clone(), id, self.event_queue.clone()); - self.progress_completely(&mut diff_machine)?; + self.progress_completely(realdom, &mut diff_machine)?; - Ok(diff_machine.consume()) + Ok(()) } /// Consume the event queue, descending depth-first. /// Only ever run each component once. /// /// The DiffMachine logs its progress as it goes which might be useful for certain types of renderers. - pub(crate) fn progress_completely<'s>( + pub(crate) fn progress_completely<'s, Dom: RealDom>( &'s mut self, - diff_machine: &'_ mut DiffMachine<'s>, + realdom: &mut Dom, + diff_machine: &'_ mut DiffMachine, ) -> Result<()> { // Add this component to the list of components that need to be difed // #[allow(unused_assignments)] @@ -302,7 +314,8 @@ impl VirtualDom { cur_component.run_scope()?; // diff_machine.change_list.load_known_root(1); - diff_machine.diff_node(cur_component.old_frame(), cur_component.next_frame()); + let (old, new) = cur_component.get_frames_mut(); + diff_machine.diff_node(realdom, old, new); // cur_height = cur_component.height; @@ -530,6 +543,12 @@ impl Scope { Ok(()) } + fn get_frames_mut<'bump>( + &'bump mut self, + ) -> (&'bump mut VNode<'bump>, &'bump mut VNode<'bump>) { + todo!() + } + pub fn next_frame<'bump>(&'bump self) -> &'bump VNode<'bump> { self.frames.current_head_node() } From 79127ea6cdabf62bf91e777aaacb563e3aa5e619 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 20 Jun 2021 02:16:42 -0400 Subject: [PATCH 03/20] wip: moving to IDs --- packages/core/src/diff.rs | 325 +++++++++++++++---------------- packages/core/src/nodes.rs | 4 +- packages/core/src/virtual_dom.rs | 18 +- packages/web/src/lib.rs | 1 + packages/web/src/new.rs | 2 + 5 files changed, 176 insertions(+), 174 deletions(-) create mode 100644 packages/web/src/new.rs diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 2f09fada6..30b2e1628 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -34,47 +34,48 @@ use std::{ /// The accompanying "real dom" exposes an imperative API for controlling the UI layout /// /// Instead of having handles directly over nodes, Dioxus uses simple u32s as node IDs. -/// This allows layouts with up to 4,294,967,295 nodes. If we +/// This allows layouts with up to 4,294,967,295 nodes. If we use nohasher, then retrieving is very fast. + pub trait RealDom { - fn delete_root(&mut self, root: RealDomNode); + fn delete_root(&self, root: RealDomNode); // =========== // Create // =========== /// Create a new text node and push it on to the top of the stack - fn create_text_node(&mut self, text: &str) -> RealDomNode; + fn create_text_node(&self, text: &str) -> RealDomNode; /// Create a new text node and push it on to the top of the stack - fn create_element(&mut self, tag: &str) -> RealDomNode; + fn create_element(&self, tag: &str) -> RealDomNode; /// Create a new namespaced element and push it on to the top of the stack - fn create_element_ns(&mut self, tag: &str, namespace: &str) -> RealDomNode; + fn create_element_ns(&self, tag: &str, namespace: &str) -> RealDomNode; fn append_node(&self, child: RealDomNode, parent: RealDomNode); // =========== // Remove // =========== - fn remove_node(&mut self, node: RealDomNode); + fn remove_node(&self, node: RealDomNode); - fn remove_all_children(&mut self, node: RealDomNode); + fn remove_all_children(&self, node: RealDomNode); // =========== // Replace // =========== - fn replace_node_with(&mut self, old: RealDomNode, new: RealDomNode); + fn replace_node_with(&self, old: RealDomNode, new: RealDomNode); - fn new_event_listener(&mut self, node: RealDomNode, event: &str); + fn new_event_listener(&self, node: RealDomNode, event: &str); - fn set_inner_text(&mut self, node: RealDomNode, text: &str); + fn set_inner_text(&self, node: RealDomNode, text: &str); - fn set_class(&mut self, node: RealDomNode); + fn set_class(&self, node: RealDomNode); - fn set_attr(&mut self, node: RealDomNode, name: &str, value: &str); + fn set_attr(&self, node: RealDomNode, name: &str, value: &str); - fn remove_attr(&mut self, node: RealDomNode); + fn remove_attr(&self, node: RealDomNode); - fn raw_node_as_any_mut(&mut self) -> &mut dyn Any; + fn raw_node_as_any_mut(&self) -> &mut dyn Any; } /// The DiffState is a cursor internal to the VirtualDOM's diffing algorithm that allows persistence of state while @@ -89,7 +90,8 @@ pub trait RealDom { /// The order of these re-entrances is stored in the DiffState itself. The DiffState comes pre-loaded with a set of components /// that were modified by the eventtrigger. This prevents doubly evaluating components if they were both updated via /// subscriptions and props changes. -pub struct DiffMachine { +pub struct DiffMachine<'a, Dom: RealDom> { + pub dom: &'a mut Dom, pub cur_idx: ScopeIdx, pub diffed: FxHashSet, pub components: ScopeArena, @@ -105,10 +107,16 @@ fn next_id() -> u32 { out } -impl DiffMachine { - pub fn new(components: ScopeArena, cur_idx: ScopeIdx, event_queue: EventQueue) -> Self { +impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { + pub fn new( + dom: &'a mut Dom, + components: ScopeArena, + cur_idx: ScopeIdx, + event_queue: EventQueue, + ) -> Self { Self { components, + dom, cur_idx, event_queue, diffed: FxHashSet::default(), @@ -116,13 +124,8 @@ impl DiffMachine { } } - pub fn diff_node<'a, Dom: RealDom>( - &mut self, - dom: &mut Dom, - old_node: &mut VNode<'a>, - new_node: &mut VNode<'a>, - ) { - // pub fn diff_node(&mut self, old: &VNode<'a>, new: &VNode<'a>) { + pub fn diff_node(&self, old_node: &mut VNode<'a>, new_node: &mut VNode<'a>) { + // pub fn diff_node(&self, old: &VNode<'a>, new: &VNode<'a>) { /* For each valid case, we "commit traversal", meaning we save this current position in the tree. Then, we diff and queue an edit event (via chagelist). s single trees - when components show up, we save that traversal and then re-enter later. @@ -132,30 +135,33 @@ impl DiffMachine { (VNode::Text(old), VNode::Text(new)) => { new.dom_id = old.dom_id; if old.text != new.text { - self.realdom.set_inner_text(new.dom_id, new.text); + self.dom.set_inner_text(new.dom_id.get(), new.text); } } (VNode::Text(old), VNode::Element(new)) => { - // // self.realdom.commit_traversal(); - self.create(new_node); - self.realdom.replace_node_with(old.dom_id, old.dom_id); - // self.realdom.replace_with(); + // // self.dom.commit_traversal(); + self.create_and_repalce(new_node, old.dom_id.get()) + // self.create(new_node); + // self.dom.replace_node_with(old.dom_id, old.dom_id); + // self.dom.replace_with(); } (VNode::Element(old), VNode::Text(new)) => { - // // self.realdom.commit_traversal(); - self.create(new_node); - self.realdom.replace_node_with(old.dom_id, new.dom_id); - // self.realdom.replace_with(); + // // self.dom.commit_traversal(); + self.create_and_repalce(new_node, old.dom_id.get()) + // self.create(new_node); + // self.dom.replace_node_with(old.dom_id, new.dom_id); + // self.dom.replace_with(); } (VNode::Element(old), VNode::Element(new)) => { // If the element type is completely different, the element needs to be re-rendered completely if new.tag_name != old.tag_name || new.namespace != old.namespace { - // // self.realdom.commit_traversal(); - // self.realdom.replace_with(); - self.realdom.replace_node_with(old.dom_id, new.dom_id); + // // self.dom.commit_traversal(); + // self.dom.replace_with(); + self.dom + .replace_node_with(old.dom_id.get(), new.dom_id.get()); return; } @@ -186,7 +192,7 @@ impl DiffMachine { }; if should_render { - // // self.realdom.commit_traversal(); + // // self.dom.commit_traversal(); self.components .with_scope(scope_id.unwrap(), |f| { f.run_scope().unwrap(); @@ -204,10 +210,11 @@ impl DiffMachine { // A new component has shown up! We need to destroy the old node // Wipe the old one and plant the new one - // self.realdom.commit_traversal(); - self.create(new_node); - // self.realdom.replace_node_with(old.dom_id, new.dom_id); - self.realdom.replace_with(); + // self.dom.commit_traversal(); + // self.dom.replace_node_with(old.dom_id, new.dom_id); + // self.create(new_node); + // self.dom.replace_with(); + self.create_and_repalce(new_node, old.mounted_root.get()); // Now we need to remove the old scope and all of its descendents let old_scope = old.ass_scope.borrow().as_ref().unwrap().clone(); @@ -216,17 +223,19 @@ impl DiffMachine { } // todo: knock out any listeners - (_, VNode::Component(_)) => { - // self.realdom.commit_traversal(); - self.create(new_node); - self.realdom.replace_with(); + (old, VNode::Component(_)) => { + // self.dom.commit_traversal(); + // self.create(new_node); + // self.dom.replace_with(); + self.create_and_repalce(new_node, old.dom_id.get()) } // A component is being torn down in favor of a non-component node (VNode::Component(_old), _) => { - // self.realdom.commit_traversal(); - self.create(new_node); - self.realdom.replace_with(); + // self.dom.commit_traversal(); + // self.create(new_node); + // self.dom.replace_with(); + self.create_and_repalce(new_node, old.dom_id.get()) // Destroy the original scope and any of its children self.destroy_scopes(_old.ass_scope.borrow().unwrap()); @@ -260,26 +269,18 @@ impl DiffMachine { } } - // Emit instructions to create the given virtual node. - // - // The change list stack may have any shape upon entering this function: - // - // [...] - // - // When this function returns, the new node is on top of the change list stack: - // - // [... node] - fn create<'a, Dom: RealDom>( - &mut self, - dom: &mut Dom, - node: &mut VNode<'a>, - parent: RealDomNode, - ) { - // debug_assert!(self.realdom.traversal_is_committed()); + // create a node and replace another node + // this method doesn't work with + fn create_and_repalce(&self, node: &mut VNode<'a>, parent: RealDomNode) {} + + // create and append creates the series of elements and immediately appends them to whatever parent is provided + // this way we can handle a series of children + fn create_and_append(&self, node: &mut VNode<'a>, parent: RealDomNode) { + // debug_assert!(self.dom.traversal_is_committed()); match node { VNode::Text(text) => { - let real_id = self.realdom.create_text_node(text.text); - text.dom_id = real_id; + let real_id = self.dom.create_text_node(text.text); + text.dom_id.set(real_id); } VNode::Element(&el) => { let VElement { @@ -293,21 +294,21 @@ impl DiffMachine { } = el; // log::info!("Creating {:#?}", node); let real_id = if let Some(namespace) = namespace { - self.realdom.create_element_ns(tag_name, namespace) + self.dom.create_element_ns(tag_name, namespace) } else { - self.realdom.create_element(tag_name) + self.dom.create_element(tag_name) }; el.dom_id = real_id; listeners.iter().enumerate().for_each(|(_id, listener)| { todo!() - // self.realdom + // dom // .new_event_listener(listener.event, listener.scope, listener.id) }); for attr in attributes { todo!() - // self.realdom + // dom // .set_attribute(&attr.name, &attr.value, namespace.is_some()); } @@ -319,7 +320,7 @@ impl DiffMachine { // parent. if children.len() == 1 { if let VNode::Text(text) = children[0] { - self.realdom.set_inner_text(real_id, text.text); + self.dom.set_inner_text(real_id, text.text); return; } } @@ -330,16 +331,16 @@ impl DiffMachine { // do nothing // fragments append themselves } else { - self.realdom.append_child(); + self.dom.append_child(); } } } VNode::Component(component) => { - self.realdom.create_text_node("placeholder for vcomponent"); + self.dom.create_text_node("placeholder for vcomponent"); // let root_id = next_id(); - // self.realdom.save_known_root(root_id); + // self.dom.save_known_root(root_id); log::debug!("Mounting a new component"); let caller: Weak = Rc::downgrade(&component.caller); @@ -402,7 +403,7 @@ impl DiffMachine { for child in frag.children { todo!() // self.create(child); - // self.realdom.append_child(); + // self.dom.append_child(); } } @@ -416,7 +417,7 @@ impl DiffMachine { /// /// Calling this will run the destuctors on all hooks in the tree. /// It will also add the destroyed nodes to the `seen_nodes` cache to prevent them from being renderered. - fn destroy_scopes(&mut self, old_scope: ScopeIdx) { + fn destroy_scopes(&self, old_scope: ScopeIdx) { let mut nodes_to_delete = vec![old_scope]; let mut scopes_to_explore = vec![old_scope]; @@ -451,9 +452,9 @@ impl DiffMachine { // [... node] // // The change list stack is left unchanged. - fn diff_listeners(&mut self, old: &[Listener<'_>], new: &[Listener<'_>]) { + fn diff_listeners(&self, old: &[Listener<'_>], new: &[Listener<'_>]) { if !old.is_empty() || !new.is_empty() { - // self.realdom.commit_traversal(); + // self.dom.commit_traversal(); } 'outer1: for (_l_idx, new_l) in new.iter().enumerate() { @@ -468,8 +469,8 @@ impl DiffMachine { for old_l in old { if new_l.event == old_l.event { if new_l.id != old_l.id { - self.realdom.remove_event_listener(event_type); - self.realdom + self.dom.remove_event_listener(event_type); + self.dom .update_event_listener(event_type, new_l.scope, new_l.id) } @@ -477,7 +478,7 @@ impl DiffMachine { } } - self.realdom + self.dom .new_event_listener(event_type, new_l.scope, new_l.id); } @@ -487,7 +488,7 @@ impl DiffMachine { continue 'outer2; } } - self.realdom.remove_event_listener(old_l.event); + self.dom.remove_event_listener(old_l.event); } } @@ -498,12 +499,7 @@ impl DiffMachine { // [... node] // // The change list stack is left unchanged. - fn diff_attr<'a>( - &mut self, - old: &'a [Attribute<'a>], - new: &'a [Attribute<'a>], - is_namespaced: bool, - ) { + fn diff_attr(&self, old: &'a [Attribute<'a>], new: &'a [Attribute<'a>], is_namespaced: bool) { // Do O(n^2) passes to add/update and remove attributes, since // there are almost always very few attributes. // @@ -511,19 +507,16 @@ impl DiffMachine { // With the Rsx and Html macros, this will almost always be the case 'outer: for new_attr in new { if new_attr.is_volatile() { - // self.realdom.commit_traversal(); - self.realdom + // self.dom.commit_traversal(); + self.dom .set_attribute(new_attr.name, new_attr.value, is_namespaced); } else { for old_attr in old { if old_attr.name == new_attr.name { if old_attr.value != new_attr.value { - // self.realdom.commit_traversal(); - self.realdom.set_attribute( - new_attr.name, - new_attr.value, - is_namespaced, - ); + // self.dom.commit_traversal(); + self.dom + .set_attribute(new_attr.name, new_attr.value, is_namespaced); } continue 'outer; } else { @@ -531,8 +524,8 @@ impl DiffMachine { } } - // self.realdom.commit_traversal(); - self.realdom + // self.dom.commit_traversal(); + self.dom .set_attribute(new_attr.name, new_attr.value, is_namespaced); } } @@ -544,8 +537,8 @@ impl DiffMachine { } } - // self.realdom.commit_traversal(); - self.realdom.remove_attribute(old_attr.name); + // self.dom.commit_traversal(); + self.dom.remove_attribute(old_attr.name); } } @@ -557,10 +550,10 @@ impl DiffMachine { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_children<'a>(&mut self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { + fn diff_children(&self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { if new.is_empty() { if !old.is_empty() { - // self.realdom.commit_traversal(); + // self.dom.commit_traversal(); self.remove_all_children(old); } return; @@ -573,8 +566,8 @@ impl DiffMachine { } (_, &VNode::Text(text)) => { - // self.realdom.commit_traversal(); - self.realdom.set_text(text); + // self.dom.commit_traversal(); + self.dom.set_text(text); return; } @@ -585,7 +578,7 @@ impl DiffMachine { if old.is_empty() { if !new.is_empty() { - // self.realdom.commit_traversal(); + // self.dom.commit_traversal(); self.create_and_append_children(new); } return; @@ -604,9 +597,9 @@ impl DiffMachine { ); if new_is_keyed && old_is_keyed { - let t = self.realdom.next_temporary(); + let t = self.dom.next_temporary(); self.diff_keyed_children(old, new); - self.realdom.set_next_temporary(t); + self.dom.set_next_temporary(t); } else { self.diff_non_keyed_children(old, new); } @@ -633,7 +626,7 @@ impl DiffMachine { // [... parent] // // Upon exiting, the change list stack is in the same state. - fn diff_keyed_children<'a>(&mut self, old: &[VNode<'a>], new: &[VNode<'a>]) { + fn diff_keyed_children(&self, old: &[VNode<'a>], new: &[VNode<'a>]) { // if cfg!(debug_assertions) { // let mut keys = fxhash::FxHashSet::default(); // let mut assert_unique_keys = |children: &[VNode]| { @@ -716,8 +709,8 @@ impl DiffMachine { // [... parent] // // Upon exit, the change list stack is the same. - fn diff_keyed_prefix<'a>(&mut self, old: &[VNode<'a>], new: &[VNode<'a>]) -> KeyedPrefixResult { - // self.realdom.go_down(); + fn diff_keyed_prefix(&self, old: &[VNode<'a>], new: &[VNode<'a>]) -> KeyedPrefixResult { + // self.dom.go_down(); let mut shared_prefix_count = 0; for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() { @@ -725,7 +718,7 @@ impl DiffMachine { break; } - self.realdom.go_to_sibling(i); + self.dom.go_to_sibling(i); self.diff_node(old, new); @@ -735,8 +728,8 @@ impl DiffMachine { // If that was all of the old children, then create and append the remaining // new children and we're finished. if shared_prefix_count == old.len() { - self.realdom.go_up(); - // self.realdom.commit_traversal(); + self.dom.go_up(); + // self.dom.commit_traversal(); self.create_and_append_children(&new[shared_prefix_count..]); return KeyedPrefixResult::Finished; } @@ -744,13 +737,13 @@ impl DiffMachine { // And if that was all of the new children, then remove all of the remaining // old children and we're finished. if shared_prefix_count == new.len() { - self.realdom.go_to_sibling(shared_prefix_count); - // self.realdom.commit_traversal(); + self.dom.go_to_sibling(shared_prefix_count); + // self.dom.commit_traversal(); self.remove_self_and_next_siblings(&old[shared_prefix_count..]); return KeyedPrefixResult::Finished; } - self.realdom.go_up(); + self.dom.go_up(); KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) } @@ -767,8 +760,8 @@ impl DiffMachine { // [... parent] // // Upon exit from this function, it will be restored to that same state. - fn diff_keyed_middle<'a>( - &mut self, + fn diff_keyed_middle( + &self, old: &[VNode<'a>], mut new: &[VNode<'a>], shared_prefix_count: usize, @@ -811,11 +804,11 @@ impl DiffMachine { // afresh. if shared_suffix_count == 0 && shared_keys.is_empty() { if shared_prefix_count == 0 { - // self.realdom.commit_traversal(); + // self.dom.commit_traversal(); self.remove_all_children(old); } else { - self.realdom.go_down_to_child(shared_prefix_count); - // self.realdom.commit_traversal(); + self.dom.go_down_to_child(shared_prefix_count); + // self.dom.commit_traversal(); self.remove_self_and_next_siblings(&old[shared_prefix_count..]); } @@ -837,8 +830,8 @@ impl DiffMachine { .unwrap_or(old.len()); if end - start > 0 { - // self.realdom.commit_traversal(); - let mut t = self.realdom.save_children_to_temporaries( + // self.dom.commit_traversal(); + let mut t = self.dom.save_children_to_temporaries( shared_prefix_count + start, shared_prefix_count + end, ); @@ -863,8 +856,8 @@ impl DiffMachine { if !shared_keys.contains(&old_child.key()) { // registry.remove_subtree(old_child); // todo - // self.realdom.commit_traversal(); - self.realdom.remove_child(i + shared_prefix_count); + // self.dom.commit_traversal(); + self.dom.remove_child(i + shared_prefix_count); removed_count += 1; } } @@ -907,7 +900,7 @@ impl DiffMachine { // shared suffix to the change list stack. // // [... parent] - self.realdom + self.dom .go_down_to_child(old_shared_suffix_start - removed_count); // [... parent first_child_of_shared_suffix] } else { @@ -923,29 +916,29 @@ impl DiffMachine { let old_index = new_index_to_old_index[last_index]; let temp = old_index_to_temp[old_index]; // [... parent] - self.realdom.go_down_to_temp_child(temp); + self.dom.go_down_to_temp_child(temp); // [... parent last] self.diff_node(&old[old_index], last); if new_index_is_in_lis.contains(&last_index) { // Don't move it, since it is already where it needs to be. } else { - // self.realdom.commit_traversal(); + // self.dom.commit_traversal(); // [... parent last] - self.realdom.append_child(); + self.dom.append_child(); // [... parent] - self.realdom.go_down_to_temp_child(temp); + self.dom.go_down_to_temp_child(temp); // [... parent last] } } else { - // self.realdom.commit_traversal(); + // self.dom.commit_traversal(); // [... parent] self.create(last); // [... parent last] - self.realdom.append_child(); + self.dom.append_child(); // [... parent] - self.realdom.go_down_to_reverse_child(0); + self.dom.go_down_to_reverse_child(0); // [... parent last] } } @@ -954,11 +947,11 @@ impl DiffMachine { let old_index = new_index_to_old_index[new_index]; if old_index == u32::MAX as usize { debug_assert!(!shared_keys.contains(&new_child.key())); - // self.realdom.commit_traversal(); + // self.dom.commit_traversal(); // [... parent successor] self.create(new_child); // [... parent successor new_child] - self.realdom.insert_before(); + self.dom.insert_before(); // [... parent new_child] } else { debug_assert!(shared_keys.contains(&new_child.key())); @@ -967,14 +960,14 @@ impl DiffMachine { if new_index_is_in_lis.contains(&new_index) { // [... parent successor] - self.realdom.go_to_temp_sibling(temp); + self.dom.go_to_temp_sibling(temp); // [... parent new_child] } else { - // self.realdom.commit_traversal(); + // self.dom.commit_traversal(); // [... parent successor] - self.realdom.push_temporary(temp); + self.dom.push_temporary(temp); // [... parent successor new_child] - self.realdom.insert_before(); + self.dom.insert_before(); // [... parent new_child] } @@ -983,7 +976,7 @@ impl DiffMachine { } // [... parent child] - self.realdom.go_up(); + self.dom.go_up(); // [... parent] } @@ -994,8 +987,8 @@ impl DiffMachine { // [... parent] // // When this function exits, the change list stack remains the same. - fn diff_keyed_suffix<'a>( - &mut self, + fn diff_keyed_suffix( + &self, old: &[VNode<'a>], new: &[VNode<'a>], new_shared_suffix_start: usize, @@ -1004,16 +997,16 @@ impl DiffMachine { debug_assert!(!old.is_empty()); // [... parent] - self.realdom.go_down(); + self.dom.go_down(); // [... parent new_child] for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() { - self.realdom.go_to_sibling(new_shared_suffix_start + i); + self.dom.go_to_sibling(new_shared_suffix_start + i); self.diff_node(old_child, new_child); } // [... parent] - self.realdom.go_up(); + self.dom.go_up(); } // Diff children that are not keyed. @@ -1024,18 +1017,18 @@ impl DiffMachine { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children<'a>(&mut self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { + fn diff_non_keyed_children(&self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { // Handled these cases in `diff_children` before calling this function. debug_assert!(!new.is_empty()); debug_assert!(!old.is_empty()); // [... parent] - self.realdom.go_down(); + self.dom.go_down(); // [... parent child] for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() { // [... parent prev_child] - self.realdom.go_to_sibling(i); + self.dom.go_to_sibling(i); // [... parent this_child] self.diff_node(old_child, new_child); } @@ -1044,9 +1037,9 @@ impl DiffMachine { // old.len > new.len -> removing some nodes Ordering::Greater => { // [... parent prev_child] - self.realdom.go_to_sibling(new.len()); + self.dom.go_to_sibling(new.len()); // [... parent first_child_to_remove] - // self.realdom.commit_traversal(); + // self.dom.commit_traversal(); // support::remove_self_and_next_siblings(state, &old[new.len()..]); self.remove_self_and_next_siblings(&old[new.len()..]); // [... parent] @@ -1054,15 +1047,15 @@ impl DiffMachine { // old.len < new.len -> adding some nodes Ordering::Less => { // [... parent last_child] - self.realdom.go_up(); + self.dom.go_up(); // [... parent] - // self.realdom.commit_traversal(); + // self.dom.commit_traversal(); self.create_and_append_children(&new[old.len()..]); } // old.len == new.len -> no nodes added/removed, but πerhaps changed Ordering::Equal => { // [... parent child] - self.realdom.go_up(); + self.dom.go_up(); // [... parent] } } @@ -1079,15 +1072,16 @@ impl DiffMachine { // [... parent] // // When this function returns, the change list stack is in the same state. - pub fn remove_all_children<'a>(&mut self, old: &[VNode<'a>]) { - // debug_assert!(self.realdom.traversal_is_committed()); + pub fn remove_all_children(&self, old: &[VNode<'a>]) { + // debug_assert!(self.dom.traversal_is_committed()); log::debug!("REMOVING CHILDREN"); for _child in old { // registry.remove_subtree(child); } // Fast way to remove all children: set the node's textContent to an empty // string. - self.realdom.set_text(""); + todo!() + // self.dom.set_inner_text(""); } // Create the given children and append them to the parent node. @@ -1097,11 +1091,12 @@ impl DiffMachine { // [... parent] // // When this function returns, the change list stack is in the same state. - pub fn create_and_append_children<'a>(&mut self, new: &[VNode<'a>]) { - // debug_assert!(self.realdom.traversal_is_committed()); + pub fn create_and_append_children(&self, new: &[VNode<'a>]) { + // debug_assert!(self.dom.traversal_is_committed()); for child in new { - self.create(child); - self.realdom.append_child(); + self.create_and_append(node, parent) + // self.create(child); + // self.dom.append_child(); } } @@ -1114,11 +1109,11 @@ impl DiffMachine { // After the function returns, the child is no longer on the change list stack: // // [... parent] - pub fn remove_self_and_next_siblings<'a>(&mut self, old: &[VNode<'a>]) { - // debug_assert!(self.realdom.traversal_is_committed()); + pub fn remove_self_and_next_siblings(&self, old: &[VNode<'a>]) { + // debug_assert!(self.dom.traversal_is_committed()); for child in old { if let VNode::Component(vcomp) = child { - // self.realdom + // dom // .create_text_node("placeholder for vcomponent"); todo!() @@ -1129,7 +1124,7 @@ impl DiffMachine { // }) // let id = get_id(); // *component.stable_addr.as_ref().borrow_mut() = Some(id); - // self.realdom.save_known_root(id); + // self.dom.save_known_root(id); // let scope = Rc::downgrade(&component.ass_scope); // self.lifecycle_events.push_back(LifeCycleEvent::Mount { // caller: Rc::downgrade(&component.caller), @@ -1140,7 +1135,7 @@ impl DiffMachine { // registry.remove_subtree(child); } - self.realdom.remove_self_and_next_siblings(); + self.dom.remove_self_and_next_siblings(); } } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 17f1ba9f1..dd1bc68d6 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -234,7 +234,7 @@ pub type VCompAssociatedScope = Option; pub struct VComponent<'src> { pub key: NodeKey<'src>, - pub mounted_root: RealDomNode, + pub mounted_root: Cell, pub ass_scope: RefCell, // pub comparator: Rc bool + 'src>, @@ -335,7 +335,7 @@ impl<'a> VComponent<'a> { raw_props, children, caller, - mounted_root: RealDomNode::empty(), + mounted_root: Cell::new(RealDomNode::empty()), } } } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index c65f3d84a..1bea9ca0f 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -192,6 +192,7 @@ impl VirtualDom { /// Currently this doesn't do what we want it to do pub fn rebuild<'s, Dom: RealDom>(&'s mut self, realdom: &mut Dom) -> Result<()> { let mut diff_machine = DiffMachine::new( + realdom, self.components.clone(), self.base_scope, self.event_queue.clone(), @@ -206,7 +207,7 @@ impl VirtualDom { let update = &base.event_channel; update(); - self.progress_completely(realdom, &mut diff_machine)?; + self.progress_completely(&mut diff_machine)?; Ok(()) } @@ -262,10 +263,14 @@ impl VirtualDom { self.components.try_get_mut(id)?.call_listener(event)?; - let mut diff_machine = - DiffMachine::new(self.components.clone(), id, self.event_queue.clone()); + let mut diff_machine = DiffMachine::new( + realdom, + self.components.clone(), + id, + self.event_queue.clone(), + ); - self.progress_completely(realdom, &mut diff_machine)?; + self.progress_completely(&mut diff_machine)?; Ok(()) } @@ -276,8 +281,7 @@ impl VirtualDom { /// The DiffMachine logs its progress as it goes which might be useful for certain types of renderers. pub(crate) fn progress_completely<'s, Dom: RealDom>( &'s mut self, - realdom: &mut Dom, - diff_machine: &'_ mut DiffMachine, + diff_machine: &'_ mut DiffMachine<'s, Dom>, ) -> Result<()> { // Add this component to the list of components that need to be difed // #[allow(unused_assignments)] @@ -315,7 +319,7 @@ impl VirtualDom { // diff_machine.change_list.load_known_root(1); let (old, new) = cur_component.get_frames_mut(); - diff_machine.diff_node(realdom, old, new); + diff_machine.diff_node(old, new); // cur_height = cur_component.height; diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 104e6a0b9..9f1a583f1 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -13,6 +13,7 @@ use dioxus_core::{events::EventTrigger, prelude::FC}; pub use dioxus_core::prelude; pub mod interpreter; +pub mod new; /// The `WebsysRenderer` provides a way of rendering a Dioxus Virtual DOM to the browser's DOM. /// Under the hood, we leverage WebSys and interact directly with the DOM diff --git a/packages/web/src/new.rs b/packages/web/src/new.rs new file mode 100644 index 000000000..1c94305f3 --- /dev/null +++ b/packages/web/src/new.rs @@ -0,0 +1,2 @@ +pub struct WebsysDom {} +impl WebsysDom {} From cff0547f1a3e2e28ba1946f347c161078818ac66 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 21 Jun 2021 00:52:37 -0400 Subject: [PATCH 04/20] wip: get diff compiling Currently working through the normalization process. Occasionally, we want to iterate through all the nodes that we know have a real image in the dom. However, fragments and components don't directly have a mirror in the dom. This commit is exploring the concept of a custom iterator that explores every node in an array of nodes, returning only valid nodes which may be mounted to the dom. A big issue we're working through is heavily nested rootless nodes - something not terribly common but important nonetheless. Inferno, React, and Preact all perform a mutative-form of normalization which alter the children list before comparing to the previous. Mostly, we're concerned about fragments in lists and heavily nested components that do not render real elements. --- packages/core/src/diff.rs | 938 ++++++++++++++++--------------- packages/core/src/events.rs | 2 +- packages/core/src/lib.rs | 2 +- packages/core/src/nodebuilder.rs | 45 +- packages/core/src/nodes.rs | 119 +++- packages/core/src/virtual_dom.rs | 20 - 6 files changed, 644 insertions(+), 482 deletions(-) diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 30b2e1628..409ca6c84 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,4 +1,5 @@ //! This module contains the stateful DiffMachine and all methods to diff VNodes, their properties, and their children. +//! The DiffMachine calculates the diffs between the old and new frames, updates the new nodes, and modifies the real dom. //! //! Notice: //! ------ @@ -36,45 +37,40 @@ use std::{ /// Instead of having handles directly over nodes, Dioxus uses simple u32s as node IDs. /// This allows layouts with up to 4,294,967,295 nodes. If we use nohasher, then retrieving is very fast. +/// The "RealDom" abstracts over the... real dom. Elements are mapped by ID. The RealDom is inteded to maintain a stack +/// of real nodes as the diffing algorithm descenes through the tree. This means that whatever is on top of the stack +/// will receive modifications. However, instead of using child-based methods for descending through the tree, we instead +/// ask the RealDom to either push or pop real nodes onto the stack. This saves us the indexing cost while working on a +/// single node pub trait RealDom { - fn delete_root(&self, root: RealDomNode); + // Navigation + fn push_root(&self, root: RealDomNode); + fn pop(&self); - // =========== - // Create - // =========== - /// Create a new text node and push it on to the top of the stack + // Add Nodes to the dom + fn append_child(&self); + fn replace_with(&self); + + // Remove Nodesfrom the dom + fn remove(&self); + fn remove_all_children(&self); + + // Create fn create_text_node(&self, text: &str) -> RealDomNode; - - /// Create a new text node and push it on to the top of the stack fn create_element(&self, tag: &str) -> RealDomNode; - - /// Create a new namespaced element and push it on to the top of the stack fn create_element_ns(&self, tag: &str, namespace: &str) -> RealDomNode; - fn append_node(&self, child: RealDomNode, parent: RealDomNode); + // events + fn new_event_listener(&self, event: &str, scope: ScopeIdx, id: usize); + // fn new_event_listener(&self, event: &str); + fn remove_event_listener(&self, event: &str); - // =========== - // Remove - // =========== - fn remove_node(&self, node: RealDomNode); - - fn remove_all_children(&self, node: RealDomNode); - - // =========== - // Replace - // =========== - fn replace_node_with(&self, old: RealDomNode, new: RealDomNode); - - fn new_event_listener(&self, node: RealDomNode, event: &str); - - fn set_inner_text(&self, node: RealDomNode, text: &str); - - fn set_class(&self, node: RealDomNode); - - fn set_attr(&self, node: RealDomNode, name: &str, value: &str); - - fn remove_attr(&self, node: RealDomNode); + // modify + fn set_text(&self, text: &str); + fn set_attribute(&self, name: &str, value: &str, is_namespaced: bool); + fn remove_attribute(&self, name: &str); + // node ref fn raw_node_as_any_mut(&self) -> &mut dyn Any; } @@ -99,13 +95,13 @@ pub struct DiffMachine<'a, Dom: RealDom> { pub seen_nodes: FxHashSet, } -// todo: see if unsafe works better -static COUNTER: Cell = Cell::new(1); -fn next_id() -> u32 { - let out = COUNTER.get(); - COUNTER.set(out + 1); - out -} +// // todo: see if unsafe works better +// static COUNTER: Cell = Cell::new(1); +// fn next_id() -> u32 { +// let out = COUNTER.get(); +// COUNTER.set(out + 1); +// out +// } impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { pub fn new( @@ -123,166 +119,184 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { seen_nodes: FxHashSet::default(), } } - - pub fn diff_node(&self, old_node: &mut VNode<'a>, new_node: &mut VNode<'a>) { + // Diff the `old` node with the `new` node. Emits instructions to modify a + // physical DOM node that reflects `old` into something that reflects `new`. + // + // Upon entry to this function, the physical DOM node must be on the top of the + // change list stack: + // + // [... node] + // + // The change list stack is in the same state when this function exits. + // In the case of Fragments, the parent node is on the stack + pub fn diff_node(&mut self, old_node: &VNode<'a>, new_node: &VNode<'a>) { // pub fn diff_node(&self, old: &VNode<'a>, new: &VNode<'a>) { /* For each valid case, we "commit traversal", meaning we save this current position in the tree. Then, we diff and queue an edit event (via chagelist). s single trees - when components show up, we save that traversal and then re-enter later. When re-entering, we reuse the EditList in DiffState */ - match (old_node, new_node) { - (VNode::Text(old), VNode::Text(new)) => { - new.dom_id = old.dom_id; - if old.text != new.text { - self.dom.set_inner_text(new.dom_id.get(), new.text); - } - } - - (VNode::Text(old), VNode::Element(new)) => { - // // self.dom.commit_traversal(); - self.create_and_repalce(new_node, old.dom_id.get()) - // self.create(new_node); - // self.dom.replace_node_with(old.dom_id, old.dom_id); - // self.dom.replace_with(); - } - - (VNode::Element(old), VNode::Text(new)) => { - // // self.dom.commit_traversal(); - self.create_and_repalce(new_node, old.dom_id.get()) - // self.create(new_node); - // self.dom.replace_node_with(old.dom_id, new.dom_id); - // self.dom.replace_with(); - } - - (VNode::Element(old), VNode::Element(new)) => { - // If the element type is completely different, the element needs to be re-rendered completely - if new.tag_name != old.tag_name || new.namespace != old.namespace { - // // self.dom.commit_traversal(); - // self.dom.replace_with(); - self.dom - .replace_node_with(old.dom_id.get(), new.dom_id.get()); - return; - } - - self.diff_listeners(old.listeners, new.listeners); - self.diff_attr(old.attributes, new.attributes, new.namespace.is_some()); - self.diff_children(old.children, new.children); - } - - (VNode::Component(old), VNode::Component(new)) => { - // Make sure we're dealing with the same component (by function pointer) - if old.user_fc == new.user_fc { - // Make sure the new component vnode is referencing the right scope id - let scope_id = old.ass_scope.borrow().clone(); - *new.ass_scope.borrow_mut() = scope_id; - - // make sure the component's caller function is up to date - self.components - .with_scope(scope_id.unwrap(), |scope| { - scope.caller = Rc::downgrade(&new.caller) - }) - .unwrap(); - - // React doesn't automatically memoize, but we do. - // The cost is low enough to make it worth checking - let should_render = match old.comparator { - Some(comparator) => comparator(new), - None => true, - }; - - if should_render { - // // self.dom.commit_traversal(); - self.components - .with_scope(scope_id.unwrap(), |f| { - f.run_scope().unwrap(); - }) - .unwrap(); - // diff_machine.change_list.load_known_root(root_id); - // run the scope - // - } else { - // Component has memoized itself and doesn't need to be re-rendered. - // We still need to make sure the child's props are up-to-date. - // Don't commit traversal + match old_node { + VNode::Element(old) => match new_node { + // New node is an element, old node was en element, need to investiage more deeply + VNode::Element(new) => { + // If the element type is completely different, the element needs to be re-rendered completely + // This is an optimization React makes due to how users structure their code + if new.tag_name != old.tag_name || new.namespace != old.namespace { + self.create(new_node); + self.dom.replace_with(); + return; } - } else { - // A new component has shown up! We need to destroy the old node - // Wipe the old one and plant the new one - // self.dom.commit_traversal(); - // self.dom.replace_node_with(old.dom_id, new.dom_id); - // self.create(new_node); - // self.dom.replace_with(); - self.create_and_repalce(new_node, old.mounted_root.get()); - - // Now we need to remove the old scope and all of its descendents - let old_scope = old.ass_scope.borrow().as_ref().unwrap().clone(); - self.destroy_scopes(old_scope); + self.diff_listeners(old.listeners, new.listeners); + self.diff_attr(old.attributes, new.attributes, new.namespace.is_some()); + self.diff_children(old.children, new.children); + } + // New node is a text element, need to replace the element with a simple text node + VNode::Text(_) => { + self.create(new_node); + self.dom.replace_with(); } - } - // todo: knock out any listeners - (old, VNode::Component(_)) => { - // self.dom.commit_traversal(); - // self.create(new_node); - // self.dom.replace_with(); - self.create_and_repalce(new_node, old.dom_id.get()) - } + // New node is a component + // Make the component and replace our element on the stack with it + VNode::Component(_) => { + self.create(new_node); + self.dom.replace_with(); + } - // A component is being torn down in favor of a non-component node - (VNode::Component(_old), _) => { - // self.dom.commit_traversal(); - // self.create(new_node); - // self.dom.replace_with(); - self.create_and_repalce(new_node, old.dom_id.get()) + // New node is actually a sequence of nodes. + // We need to replace this one node with a sequence of nodes + // Not yet implement because it's kinda hairy + VNode::Fragment(_) => todo!(), - // Destroy the original scope and any of its children - self.destroy_scopes(_old.ass_scope.borrow().unwrap()); - } + // New Node is actually suspended. Todo + VNode::Suspended => todo!(), + }, - // Anything suspended is not enabled ATM - (VNode::Suspended, _) | (_, VNode::Suspended) => { - todo!("Suspended components not currently available") - } + // Old element was text + VNode::Text(old) => match new_node { + VNode::Text(new) => { + if old.text != new.text { + self.dom.set_text(new.text); + } + } + VNode::Element(_) | VNode::Component(_) => { + self.create(new_node); + self.dom.replace_with(); + } - // Fragments are special - // we actually have to remove a bunch of nodes - (VNode::Fragment(_), _) => { - todo!("Fragments not currently supported in diffing") - } + // TODO on handling these types + VNode::Fragment(_) => todo!(), + VNode::Suspended => todo!(), + }, - (VNode::Fragment(_), VNode::Fragment(_)) => { - todo!("Fragments not currently supported in diffing") - } + // Old element was a component + VNode::Component(old) => { + match new_node { + // It's something entirely different + VNode::Element(_) | VNode::Text(_) => { + self.create(new_node); + self.dom.replace_with(); + } - (old_n, VNode::Fragment(_)) => { - match old_n { - VNode::Element(_) => todo!(), - VNode::Text(_) => todo!(), + // It's also a component + VNode::Component(new) => { + match old.user_fc == new.user_fc { + // Make sure we're dealing with the same component (by function pointer) + true => { + // Make sure the new component vnode is referencing the right scope id + let scope_id = old.ass_scope.borrow().clone(); + *new.ass_scope.borrow_mut() = scope_id; + + // make sure the component's caller function is up to date + self.components + .with_scope(scope_id.unwrap(), |scope| { + scope.caller = Rc::downgrade(&new.caller) + }) + .unwrap(); + + // React doesn't automatically memoize, but we do. + // The cost is low enough to make it worth checking + let should_render = match old.comparator { + Some(comparator) => comparator(new), + None => true, + }; + + if should_render { + // // self.dom.commit_traversal(); + self.components + .with_scope(scope_id.unwrap(), |f| { + f.run_scope().unwrap(); + }) + .unwrap(); + // diff_machine.change_list.load_known_root(root_id); + // run the scope + // + } else { + // Component has memoized itself and doesn't need to be re-rendered. + // We still need to make sure the child's props are up-to-date. + // Don't commit traversal + } + } + // It's an entirely different component + false => { + // A new component has shown up! We need to destroy the old node + + // Wipe the old one and plant the new one + // self.dom.commit_traversal(); + // self.dom.replace_node_with(old.dom_id, new.dom_id); + // self.create(new_node); + // self.dom.replace_with(); + self.create(new_node); + // self.create_and_repalce(new_node, old.mounted_root.get()); + + // Now we need to remove the old scope and all of its descendents + let old_scope = old.ass_scope.borrow().as_ref().unwrap().clone(); + self.destroy_scopes(old_scope); + } + } + } VNode::Fragment(_) => todo!(), VNode::Suspended => todo!(), - VNode::Component(_) => todo!(), } - todo!("Fragments not currently supported in diffing") } + + VNode::Fragment(_) => match new_node { + VNode::Fragment(_) => todo!(), + VNode::Element(_) => todo!(), + VNode::Text(_) => todo!(), + VNode::Suspended => todo!(), + VNode::Component(_) => todo!(), + }, + + VNode::Suspended => match new_node { + VNode::Suspended => todo!(), + VNode::Element(_) => todo!(), + VNode::Text(_) => todo!(), + VNode::Fragment(_) => todo!(), + VNode::Component(_) => todo!(), + }, } } - // create a node and replace another node - // this method doesn't work with - fn create_and_repalce(&self, node: &mut VNode<'a>, parent: RealDomNode) {} - - // create and append creates the series of elements and immediately appends them to whatever parent is provided - // this way we can handle a series of children - fn create_and_append(&self, node: &mut VNode<'a>, parent: RealDomNode) { + // Emit instructions to create the given virtual node. + // + // The change list stack may have any shape upon entering this function: + // + // [...] + // + // When this function returns, the new node is on top of the change list stack: + // + // [... node] + fn create(&mut self, node: &VNode<'a>) { // debug_assert!(self.dom.traversal_is_committed()); match node { VNode::Text(text) => { let real_id = self.dom.create_text_node(text.text); text.dom_id.set(real_id); } - VNode::Element(&el) => { + VNode::Element(el) => { let VElement { key, tag_name, @@ -298,7 +312,7 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { } else { self.dom.create_element(tag_name) }; - el.dom_id = real_id; + el.dom_id.set(real_id); listeners.iter().enumerate().for_each(|(_id, listener)| { todo!() @@ -306,10 +320,9 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // .new_event_listener(listener.event, listener.scope, listener.id) }); - for attr in attributes { - todo!() - // dom - // .set_attribute(&attr.name, &attr.value, namespace.is_some()); + for attr in *attributes { + self.dom + .set_attribute(&attr.name, &attr.value, namespace.is_some()); } // Fast path: if there is a single text child, it is faster to @@ -319,14 +332,14 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // text content, and finally (3) append the text node to this // parent. if children.len() == 1 { - if let VNode::Text(text) = children[0] { - self.dom.set_inner_text(real_id, text.text); + if let VNode::Text(text) = &children[0] { + self.dom.set_text(text.text); return; } } - for child in children { - self.create(child, real_id); + for child in *children { + self.create(child); if let VNode::Fragment(_) = child { // do nothing // fragments append themselves @@ -412,12 +425,14 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { } } } +} +impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { /// Destroy a scope and all of its descendents. /// /// Calling this will run the destuctors on all hooks in the tree. /// It will also add the destroyed nodes to the `seen_nodes` cache to prevent them from being renderered. - fn destroy_scopes(&self, old_scope: ScopeIdx) { + fn destroy_scopes(&mut self, old_scope: ScopeIdx) { let mut nodes_to_delete = vec![old_scope]; let mut scopes_to_explore = vec![old_scope]; @@ -470,8 +485,9 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { if new_l.event == old_l.event { if new_l.id != old_l.id { self.dom.remove_event_listener(event_type); - self.dom - .update_event_listener(event_type, new_l.scope, new_l.id) + // TODO! we need to mess with events and assign them by RealDomNode + // self.dom + // .update_event_listener(event_type, new_l.scope, new_l.id) } continue 'outer1; @@ -550,7 +566,7 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_children(&self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { + fn diff_children(&mut self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { if new.is_empty() { if !old.is_empty() { // self.dom.commit_traversal(); @@ -560,14 +576,16 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { } if new.len() == 1 { - match (old.first(), &new[0]) { - (Some(&VNode::Text(old_text)), &VNode::Text(new_text)) if old_text == new_text => { + match (&old.first(), &new[0]) { + (Some(VNode::Text(old_vtext)), VNode::Text(new_vtext)) + if old_vtext.text == new_vtext.text => + { // Don't take this fast path... } - (_, &VNode::Text(text)) => { + (_, VNode::Text(text)) => { // self.dom.commit_traversal(); - self.dom.set_text(text); + self.dom.set_text(text.text); return; } @@ -597,9 +615,10 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { ); if new_is_keyed && old_is_keyed { - let t = self.dom.next_temporary(); - self.diff_keyed_children(old, new); - self.dom.set_next_temporary(t); + todo!("Not yet implemented a migration away from temporaries"); + // let t = self.dom.next_temporary(); + // self.diff_keyed_children(old, new); + // self.dom.set_next_temporary(t); } else { self.diff_non_keyed_children(old, new); } @@ -627,6 +646,7 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // // Upon exiting, the change list stack is in the same state. fn diff_keyed_children(&self, old: &[VNode<'a>], new: &[VNode<'a>]) { + todo!(); // if cfg!(debug_assertions) { // let mut keys = fxhash::FxHashSet::default(); // let mut assert_unique_keys = |children: &[VNode]| { @@ -710,41 +730,42 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // // Upon exit, the change list stack is the same. fn diff_keyed_prefix(&self, old: &[VNode<'a>], new: &[VNode<'a>]) -> KeyedPrefixResult { + todo!() // self.dom.go_down(); - let mut shared_prefix_count = 0; + // let mut shared_prefix_count = 0; - for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() { - if old.key() != new.key() { - break; - } + // for (i, (old, new)) in old.iter().zip(new.iter()).enumerate() { + // if old.key() != new.key() { + // break; + // } - self.dom.go_to_sibling(i); + // self.dom.go_to_sibling(i); - self.diff_node(old, new); + // self.diff_node(old, new); - shared_prefix_count += 1; - } + // shared_prefix_count += 1; + // } - // If that was all of the old children, then create and append the remaining - // new children and we're finished. - if shared_prefix_count == old.len() { - self.dom.go_up(); - // self.dom.commit_traversal(); - self.create_and_append_children(&new[shared_prefix_count..]); - return KeyedPrefixResult::Finished; - } + // // If that was all of the old children, then create and append the remaining + // // new children and we're finished. + // if shared_prefix_count == old.len() { + // self.dom.go_up(); + // // self.dom.commit_traversal(); + // self.create_and_append_children(&new[shared_prefix_count..]); + // return KeyedPrefixResult::Finished; + // } - // And if that was all of the new children, then remove all of the remaining - // old children and we're finished. - if shared_prefix_count == new.len() { - self.dom.go_to_sibling(shared_prefix_count); - // self.dom.commit_traversal(); - self.remove_self_and_next_siblings(&old[shared_prefix_count..]); - return KeyedPrefixResult::Finished; - } + // // And if that was all of the new children, then remove all of the remaining + // // old children and we're finished. + // if shared_prefix_count == new.len() { + // self.dom.go_to_sibling(shared_prefix_count); + // // self.dom.commit_traversal(); + // self.remove_self_and_next_siblings(&old[shared_prefix_count..]); + // return KeyedPrefixResult::Finished; + // } - self.dom.go_up(); - KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) + // self.dom.go_up(); + // KeyedPrefixResult::MoreWorkToDo(shared_prefix_count) } // The most-general, expensive code path for keyed children diffing. @@ -768,215 +789,216 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { shared_suffix_count: usize, old_shared_suffix_start: usize, ) { - // Should have already diffed the shared-key prefixes and suffixes. - debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); - debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); + todo!() + // // Should have already diffed the shared-key prefixes and suffixes. + // debug_assert_ne!(new.first().map(|n| n.key()), old.first().map(|o| o.key())); + // debug_assert_ne!(new.last().map(|n| n.key()), old.last().map(|o| o.key())); - // The algorithm below relies upon using `u32::MAX` as a sentinel - // value, so if we have that many new nodes, it won't work. This - // check is a bit academic (hence only enabled in debug), since - // wasm32 doesn't have enough address space to hold that many nodes - // in memory. - debug_assert!(new.len() < u32::MAX as usize); + // // The algorithm below relies upon using `u32::MAX` as a sentinel + // // value, so if we have that many new nodes, it won't work. This + // // check is a bit academic (hence only enabled in debug), since + // // wasm32 doesn't have enough address space to hold that many nodes + // // in memory. + // debug_assert!(new.len() < u32::MAX as usize); - // Map from each `old` node's key to its index within `old`. - let mut old_key_to_old_index = FxHashMap::default(); - old_key_to_old_index.reserve(old.len()); - old_key_to_old_index.extend(old.iter().enumerate().map(|(i, o)| (o.key(), i))); + // // Map from each `old` node's key to its index within `old`. + // let mut old_key_to_old_index = FxHashMap::default(); + // old_key_to_old_index.reserve(old.len()); + // old_key_to_old_index.extend(old.iter().enumerate().map(|(i, o)| (o.key(), i))); - // The set of shared keys between `new` and `old`. - let mut shared_keys = FxHashSet::default(); - // Map from each index in `new` to the index of the node in `old` that - // has the same key. - let mut new_index_to_old_index = Vec::with_capacity(new.len()); - new_index_to_old_index.extend(new.iter().map(|n| { - let key = n.key(); - if let Some(&i) = old_key_to_old_index.get(&key) { - shared_keys.insert(key); - i - } else { - u32::MAX as usize - } - })); + // // The set of shared keys between `new` and `old`. + // let mut shared_keys = FxHashSet::default(); + // // Map from each index in `new` to the index of the node in `old` that + // // has the same key. + // let mut new_index_to_old_index = Vec::with_capacity(new.len()); + // new_index_to_old_index.extend(new.iter().map(|n| { + // let key = n.key(); + // if let Some(&i) = old_key_to_old_index.get(&key) { + // shared_keys.insert(key); + // i + // } else { + // u32::MAX as usize + // } + // })); - // If none of the old keys are reused by the new children, then we - // remove all the remaining old children and create the new children - // afresh. - if shared_suffix_count == 0 && shared_keys.is_empty() { - if shared_prefix_count == 0 { - // self.dom.commit_traversal(); - self.remove_all_children(old); - } else { - self.dom.go_down_to_child(shared_prefix_count); - // self.dom.commit_traversal(); - self.remove_self_and_next_siblings(&old[shared_prefix_count..]); - } + // // If none of the old keys are reused by the new children, then we + // // remove all the remaining old children and create the new children + // // afresh. + // if shared_suffix_count == 0 && shared_keys.is_empty() { + // if shared_prefix_count == 0 { + // // self.dom.commit_traversal(); + // self.remove_all_children(old); + // } else { + // self.dom.go_down_to_child(shared_prefix_count); + // // self.dom.commit_traversal(); + // self.remove_self_and_next_siblings(&old[shared_prefix_count..]); + // } - self.create_and_append_children(new); + // self.create_and_append_children(new); - return; - } + // return; + // } - // Save each of the old children whose keys are reused in the new - // children. - let mut old_index_to_temp = vec![u32::MAX; old.len()]; - let mut start = 0; - loop { - let end = (start..old.len()) - .find(|&i| { - let key = old[i].key(); - !shared_keys.contains(&key) - }) - .unwrap_or(old.len()); + // // Save each of the old children whose keys are reused in the new + // // children. + // let mut old_index_to_temp = vec![u32::MAX; old.len()]; + // let mut start = 0; + // loop { + // let end = (start..old.len()) + // .find(|&i| { + // let key = old[i].key(); + // !shared_keys.contains(&key) + // }) + // .unwrap_or(old.len()); - if end - start > 0 { - // self.dom.commit_traversal(); - let mut t = self.dom.save_children_to_temporaries( - shared_prefix_count + start, - shared_prefix_count + end, - ); - for i in start..end { - old_index_to_temp[i] = t; - t += 1; - } - } + // if end - start > 0 { + // // self.dom.commit_traversal(); + // let mut t = self.dom.save_children_to_temporaries( + // shared_prefix_count + start, + // shared_prefix_count + end, + // ); + // for i in start..end { + // old_index_to_temp[i] = t; + // t += 1; + // } + // } - debug_assert!(end <= old.len()); - if end == old.len() { - break; - } else { - start = end + 1; - } - } + // debug_assert!(end <= old.len()); + // if end == old.len() { + // break; + // } else { + // start = end + 1; + // } + // } - // Remove any old children whose keys were not reused in the new - // children. Remove from the end first so that we don't mess up indices. - let mut removed_count = 0; - for (i, old_child) in old.iter().enumerate().rev() { - if !shared_keys.contains(&old_child.key()) { - // registry.remove_subtree(old_child); - // todo - // self.dom.commit_traversal(); - self.dom.remove_child(i + shared_prefix_count); - removed_count += 1; - } - } + // // Remove any old children whose keys were not reused in the new + // // children. Remove from the end first so that we don't mess up indices. + // let mut removed_count = 0; + // for (i, old_child) in old.iter().enumerate().rev() { + // if !shared_keys.contains(&old_child.key()) { + // // registry.remove_subtree(old_child); + // // todo + // // self.dom.commit_traversal(); + // self.dom.remove_child(i + shared_prefix_count); + // removed_count += 1; + // } + // } - // If there aren't any more new children, then we are done! - if new.is_empty() { - return; - } + // // If there aren't any more new children, then we are done! + // if new.is_empty() { + // return; + // } - // The longest increasing subsequence within `new_index_to_old_index`. This - // is the longest sequence on DOM nodes in `old` that are relatively ordered - // correctly within `new`. We will leave these nodes in place in the DOM, - // and only move nodes that are not part of the LIS. This results in the - // maximum number of DOM nodes left in place, AKA the minimum number of DOM - // nodes moved. - let mut new_index_is_in_lis = FxHashSet::default(); - new_index_is_in_lis.reserve(new_index_to_old_index.len()); - let mut predecessors = vec![0; new_index_to_old_index.len()]; - let mut starts = vec![0; new_index_to_old_index.len()]; - longest_increasing_subsequence::lis_with( - &new_index_to_old_index, - &mut new_index_is_in_lis, - |a, b| a < b, - &mut predecessors, - &mut starts, - ); + // // The longest increasing subsequence within `new_index_to_old_index`. This + // // is the longest sequence on DOM nodes in `old` that are relatively ordered + // // correctly within `new`. We will leave these nodes in place in the DOM, + // // and only move nodes that are not part of the LIS. This results in the + // // maximum number of DOM nodes left in place, AKA the minimum number of DOM + // // nodes moved. + // let mut new_index_is_in_lis = FxHashSet::default(); + // new_index_is_in_lis.reserve(new_index_to_old_index.len()); + // let mut predecessors = vec![0; new_index_to_old_index.len()]; + // let mut starts = vec![0; new_index_to_old_index.len()]; + // longest_increasing_subsequence::lis_with( + // &new_index_to_old_index, + // &mut new_index_is_in_lis, + // |a, b| a < b, + // &mut predecessors, + // &mut starts, + // ); - // Now we will iterate from the end of the new children back to the - // beginning, diffing old children we are reusing and if they aren't in the - // LIS moving them to their new destination, or creating new children. Note - // that iterating in reverse order lets us use `Node.prototype.insertBefore` - // to move/insert children. - // - // But first, we ensure that we have a child on the change list stack that - // we can `insertBefore`. We handle this once before looping over `new` - // children, so that we don't have to keep checking on every loop iteration. - if shared_suffix_count > 0 { - // There is a shared suffix after these middle children. We will be - // inserting before that shared suffix, so add the first child of that - // shared suffix to the change list stack. - // - // [... parent] - self.dom - .go_down_to_child(old_shared_suffix_start - removed_count); - // [... parent first_child_of_shared_suffix] - } else { - // There is no shared suffix coming after these middle children. - // Therefore we have to process the last child in `new` and move it to - // the end of the parent's children if it isn't already there. - let last_index = new.len() - 1; - // uhhhh why an unwrap? - let last = new.last().unwrap(); - // let last = new.last().unwrap_throw(); - new = &new[..new.len() - 1]; - if shared_keys.contains(&last.key()) { - let old_index = new_index_to_old_index[last_index]; - let temp = old_index_to_temp[old_index]; - // [... parent] - self.dom.go_down_to_temp_child(temp); - // [... parent last] - self.diff_node(&old[old_index], last); + // // Now we will iterate from the end of the new children back to the + // // beginning, diffing old children we are reusing and if they aren't in the + // // LIS moving them to their new destination, or creating new children. Note + // // that iterating in reverse order lets us use `Node.prototype.insertBefore` + // // to move/insert children. + // // + // // But first, we ensure that we have a child on the change list stack that + // // we can `insertBefore`. We handle this once before looping over `new` + // // children, so that we don't have to keep checking on every loop iteration. + // if shared_suffix_count > 0 { + // // There is a shared suffix after these middle children. We will be + // // inserting before that shared suffix, so add the first child of that + // // shared suffix to the change list stack. + // // + // // [... parent] + // self.dom + // .go_down_to_child(old_shared_suffix_start - removed_count); + // // [... parent first_child_of_shared_suffix] + // } else { + // // There is no shared suffix coming after these middle children. + // // Therefore we have to process the last child in `new` and move it to + // // the end of the parent's children if it isn't already there. + // let last_index = new.len() - 1; + // // uhhhh why an unwrap? + // let last = new.last().unwrap(); + // // let last = new.last().unwrap_throw(); + // new = &new[..new.len() - 1]; + // if shared_keys.contains(&last.key()) { + // let old_index = new_index_to_old_index[last_index]; + // let temp = old_index_to_temp[old_index]; + // // [... parent] + // self.dom.go_down_to_temp_child(temp); + // // [... parent last] + // self.diff_node(&old[old_index], last); - if new_index_is_in_lis.contains(&last_index) { - // Don't move it, since it is already where it needs to be. - } else { - // self.dom.commit_traversal(); - // [... parent last] - self.dom.append_child(); - // [... parent] - self.dom.go_down_to_temp_child(temp); - // [... parent last] - } - } else { - // self.dom.commit_traversal(); - // [... parent] - self.create(last); + // if new_index_is_in_lis.contains(&last_index) { + // // Don't move it, since it is already where it needs to be. + // } else { + // // self.dom.commit_traversal(); + // // [... parent last] + // self.dom.append_child(); + // // [... parent] + // self.dom.go_down_to_temp_child(temp); + // // [... parent last] + // } + // } else { + // // self.dom.commit_traversal(); + // // [... parent] + // self.create(last); - // [... parent last] - self.dom.append_child(); - // [... parent] - self.dom.go_down_to_reverse_child(0); - // [... parent last] - } - } + // // [... parent last] + // self.dom.append_child(); + // // [... parent] + // self.dom.go_down_to_reverse_child(0); + // // [... parent last] + // } + // } - for (new_index, new_child) in new.iter().enumerate().rev() { - let old_index = new_index_to_old_index[new_index]; - if old_index == u32::MAX as usize { - debug_assert!(!shared_keys.contains(&new_child.key())); - // self.dom.commit_traversal(); - // [... parent successor] - self.create(new_child); - // [... parent successor new_child] - self.dom.insert_before(); - // [... parent new_child] - } else { - debug_assert!(shared_keys.contains(&new_child.key())); - let temp = old_index_to_temp[old_index]; - debug_assert_ne!(temp, u32::MAX); + // for (new_index, new_child) in new.iter().enumerate().rev() { + // let old_index = new_index_to_old_index[new_index]; + // if old_index == u32::MAX as usize { + // debug_assert!(!shared_keys.contains(&new_child.key())); + // // self.dom.commit_traversal(); + // // [... parent successor] + // self.create(new_child); + // // [... parent successor new_child] + // self.dom.insert_before(); + // // [... parent new_child] + // } else { + // debug_assert!(shared_keys.contains(&new_child.key())); + // let temp = old_index_to_temp[old_index]; + // debug_assert_ne!(temp, u32::MAX); - if new_index_is_in_lis.contains(&new_index) { - // [... parent successor] - self.dom.go_to_temp_sibling(temp); - // [... parent new_child] - } else { - // self.dom.commit_traversal(); - // [... parent successor] - self.dom.push_temporary(temp); - // [... parent successor new_child] - self.dom.insert_before(); - // [... parent new_child] - } + // if new_index_is_in_lis.contains(&new_index) { + // // [... parent successor] + // self.dom.go_to_temp_sibling(temp); + // // [... parent new_child] + // } else { + // // self.dom.commit_traversal(); + // // [... parent successor] + // self.dom.push_temporary(temp); + // // [... parent successor new_child] + // self.dom.insert_before(); + // // [... parent new_child] + // } - self.diff_node(&old[old_index], new_child); - } - } + // self.diff_node(&old[old_index], new_child); + // } + // } - // [... parent child] - self.dom.go_up(); + // // [... parent child] + // self.dom.go_up(); // [... parent] } @@ -993,20 +1015,21 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { new: &[VNode<'a>], new_shared_suffix_start: usize, ) { - debug_assert_eq!(old.len(), new.len()); - debug_assert!(!old.is_empty()); + todo!() + // debug_assert_eq!(old.len(), new.len()); + // debug_assert!(!old.is_empty()); - // [... parent] - self.dom.go_down(); - // [... parent new_child] + // // [... parent] + // self.dom.go_down(); + // // [... parent new_child] - for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() { - self.dom.go_to_sibling(new_shared_suffix_start + i); - self.diff_node(old_child, new_child); - } + // for (i, (old_child, new_child)) in old.iter().zip(new.iter()).enumerate() { + // self.dom.go_to_sibling(new_shared_suffix_start + i); + // self.diff_node(old_child, new_child); + // } - // [... parent] - self.dom.go_up(); + // // [... parent] + // self.dom.go_up(); } // Diff children that are not keyed. @@ -1023,42 +1046,44 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { debug_assert!(!old.is_empty()); // [... parent] - self.dom.go_down(); + // self.dom.go_down(); + // self.dom.push_root() // [... parent child] - for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() { - // [... parent prev_child] - self.dom.go_to_sibling(i); - // [... parent this_child] - self.diff_node(old_child, new_child); - } + todo!() + // for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() { + // // [... parent prev_child] + // self.dom.go_to_sibling(i); + // // [... parent this_child] + // self.diff_node(old_child, new_child); + // } - match old.len().cmp(&new.len()) { - // old.len > new.len -> removing some nodes - Ordering::Greater => { - // [... parent prev_child] - self.dom.go_to_sibling(new.len()); - // [... parent first_child_to_remove] - // self.dom.commit_traversal(); - // support::remove_self_and_next_siblings(state, &old[new.len()..]); - self.remove_self_and_next_siblings(&old[new.len()..]); - // [... parent] - } - // old.len < new.len -> adding some nodes - Ordering::Less => { - // [... parent last_child] - self.dom.go_up(); - // [... parent] - // self.dom.commit_traversal(); - self.create_and_append_children(&new[old.len()..]); - } - // old.len == new.len -> no nodes added/removed, but πerhaps changed - Ordering::Equal => { - // [... parent child] - self.dom.go_up(); - // [... parent] - } - } + // match old.len().cmp(&new.len()) { + // // old.len > new.len -> removing some nodes + // Ordering::Greater => { + // // [... parent prev_child] + // self.dom.go_to_sibling(new.len()); + // // [... parent first_child_to_remove] + // // self.dom.commit_traversal(); + // // support::remove_self_and_next_siblings(state, &old[new.len()..]); + // self.remove_self_and_next_siblings(&old[new.len()..]); + // // [... parent] + // } + // // old.len < new.len -> adding some nodes + // Ordering::Less => { + // // [... parent last_child] + // self.dom.go_up(); + // // [... parent] + // // self.dom.commit_traversal(); + // self.create_and_append_children(&new[old.len()..]); + // } + // // old.len == new.len -> no nodes added/removed, but πerhaps changed + // Ordering::Equal => { + // // [... parent child] + // self.dom.go_up(); + // // [... parent] + // } + // } } // ====================== @@ -1072,7 +1097,7 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // [... parent] // // When this function returns, the change list stack is in the same state. - pub fn remove_all_children(&self, old: &[VNode<'a>]) { + pub fn remove_all_children(&mut self, old: &[VNode<'a>]) { // debug_assert!(self.dom.traversal_is_committed()); log::debug!("REMOVING CHILDREN"); for _child in old { @@ -1091,12 +1116,12 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // [... parent] // // When this function returns, the change list stack is in the same state. - pub fn create_and_append_children(&self, new: &[VNode<'a>]) { + pub fn create_and_append_children(&mut self, new: &[VNode<'a>]) { // debug_assert!(self.dom.traversal_is_committed()); for child in new { - self.create_and_append(node, parent) - // self.create(child); - // self.dom.append_child(); + // self.create_and_append(node, parent) + self.create(child); + self.dom.append_child(); } } @@ -1135,7 +1160,8 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // registry.remove_subtree(child); } - self.dom.remove_self_and_next_siblings(); + todo!() + // self.dom.remove_self_and_next_siblings(); } } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 063203102..d5b6a4d47 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -54,8 +54,8 @@ pub mod on { use crate::{ builder::ElementBuilder, + builder::NodeCtx, innerlude::{Attribute, Listener, VNode}, - virtual_dom::NodeCtx, }; use super::VirtualEvent; diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 3f735d9a6..61cd18494 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -58,7 +58,7 @@ pub mod prelude { pub use crate::nodebuilder::LazyNodes; pub use crate::nodebuilder::ChildrenList; - pub use crate::virtual_dom::NodeCtx; + pub use crate::nodebuilder::NodeCtx; // pub use nodes::iterables::IterableNodes; /// This type alias is an internal way of abstracting over the static functions that represent components. pub use crate::innerlude::FC; diff --git a/packages/core/src/nodebuilder.rs b/packages/core/src/nodebuilder.rs index 0819c2da0..cc60428ca 100644 --- a/packages/core/src/nodebuilder.rs +++ b/packages/core/src/nodebuilder.rs @@ -1,13 +1,15 @@ //! Helpers for building virtual DOM VNodes. -use std::{any::Any, borrow::BorrowMut, fmt::Arguments, intrinsics::transmute, u128}; +use std::{ + any::Any, borrow::BorrowMut, cell::RefCell, fmt::Arguments, intrinsics::transmute, u128, +}; use crate::{ events::VirtualEvent, innerlude::{Properties, VComponent, FC}, nodes::{Attribute, Listener, NodeKey, VNode}, prelude::{VElement, VFragment}, - virtual_dom::NodeCtx, + virtual_dom::Scope, }; /// A virtual DOM element builder. @@ -715,3 +717,42 @@ impl<'a, 'b> ChildrenList<'a, 'b> { self.children.into_bump_slice() } } + +// NodeCtx is used to build VNodes in the component's memory space. +// This struct adds metadata to the final VNode about listeners, attributes, and children +#[derive(Clone)] +pub struct NodeCtx<'a> { + pub scope_ref: &'a Scope, + pub listener_id: RefCell, +} + +impl<'a> NodeCtx<'a> { + #[inline] + pub fn bump(&self) -> &'a bumpalo::Bump { + &self.scope_ref.cur_frame().bump + } + + fn text(&self, args: Arguments) -> VNode<'a> { + text3(self.bump(), args) + } + + fn element<'b, Listeners, Attributes, Children>( + &'b self, + tag_name: &'static str, + ) -> ElementBuilder< + 'a, + 'b, + bumpalo::collections::Vec<'a, Listener<'a>>, + bumpalo::collections::Vec<'a, Attribute<'a>>, + bumpalo::collections::Vec<'a, VNode<'a>>, + > { + ElementBuilder::new(self, tag_name) + } +} + +use std::fmt::Debug; +impl Debug for NodeCtx<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Ok(()) + } +} diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index dd1bc68d6..0fdb6eef5 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -6,8 +6,8 @@ use crate::{ events::VirtualEvent, innerlude::{Context, Properties, Scope, ScopeIdx, FC}, - nodebuilder::text3, - virtual_dom::{NodeCtx, RealDomNode}, + nodebuilder::{text3, NodeCtx}, + virtual_dom::RealDomNode, }; use bumpalo::Bump; use std::{ @@ -110,6 +110,10 @@ impl<'a> VNode<'a> { VNode::Suspended => NodeKey::NONE, } } + + fn get_child(&self, id: u32) -> Option> { + todo!() + } } #[derive(Clone)] @@ -379,3 +383,114 @@ impl<'a> VFragment<'a> { Self { key, children } } } + +/// This method converts a list of nested real/virtual nodes into a stream of nodes that are definitely associated +/// with the real dom. +/// +/// Why? +/// --- +/// Fragments are seen as virtual nodes but are actually a list of possibly-real nodes. +/// JS implementations normalize their node lists when fragments are present. Here, we just create a new iterator +/// that iterates through the recursive nesting of fragments. +/// +/// Fragments are stupid and I wish we didn't need to support them. +/// +/// This iterator only supports 3 levels of nested fragments +/// +pub fn iterate_real_nodes<'a>(nodes: &'a [VNode<'a>]) -> RealNodeIterator<'a> { + RealNodeIterator::new(nodes) +} + +struct RealNodeIterator<'a> { + nodes: &'a [VNode<'a>], + + // an idx for each level of nesting + // it's highly highly unlikely to hit 4 levels of nested fragments + // so... we just don't support it + nesting_idxs: [Option; 3], +} + +impl<'a> RealNodeIterator<'a> { + fn new(nodes: &'a [VNode<'a>]) -> Self { + Self { + nodes, + nesting_idxs: [None, None, None], + } + } + + // advances the cursor to the next element, panicing if we're on the 3rd level and still finding fragments + fn advance_cursor(&mut self) { + match self.nesting_idxs { + [None, ..] => {} + } + } + + fn get_current_node(&self) -> Option<&VNode<'a>> { + match self.nesting_idxs { + [None, None, None] => None, + [Some(a), None, None] => Some(&self.nodes[a as usize]), + [Some(a), Some(b), None] => { + // + *&self.nodes[a as usize].get_child(b).as_ref() + } + [Some(a), Some(b), Some(c)] => { + // + *&self.nodes[a as usize] + .get_child(b) + .unwrap() + .get_child(c) + .as_ref() + } + } + } +} + +impl<'a> Iterator for RealNodeIterator<'a> { + type Item = &'a VNode<'a>; + + fn next(&mut self) -> Option { + todo!() + // let top_idx = self.nesting_idxs.get_mut(0).unwrap(); + // let node = &self.nodes.get_mut(*top_idx as usize); + + // if node.is_none() { + // return None; + // } + // let node = node.unwrap(); + + // match node { + // VNode::Element(_) | VNode::Text(_) => { + // *top_idx += 1; + // return Some(node); + // } + // VNode::Suspended => todo!(), + // // we need access over the scope map + // VNode::Component(_) => todo!(), + + // VNode::Fragment(frag) => { + // let nest_idx = self.nesting_idxs.get_mut(1).unwrap(); + // let node = &frag.children.get_mut(*nest_idx as usize); + // match node { + // VNode::Element(_) | VNode::Text(_) => { + // *nest_idx += 1; + // return Some(node); + // } + // VNode::Fragment(_) => todo!(), + // VNode::Suspended => todo!(), + // VNode::Component(_) => todo!(), + // } + // } + // } + } +} + +mod tests { + use crate::nodebuilder::LazyNodes; + + #[test] + fn iterate_nodes() { + // let t1 = LazyNodes::new(|b| { + // // + // }); + } +} diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 1bea9ca0f..491fdeabb 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -881,26 +881,6 @@ impl PartialOrd for HeightMarker { } } -// NodeCtx is used to build VNodes in the component's memory space. -// This struct adds metadata to the final VNode about listeners, attributes, and children -#[derive(Clone)] -pub struct NodeCtx<'a> { - pub scope_ref: &'a Scope, - pub listener_id: RefCell, -} - -impl<'a> NodeCtx<'a> { - pub fn bump(&self) -> &'a Bump { - &self.scope_ref.cur_frame().bump - } -} - -impl Debug for NodeCtx<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - Ok(()) - } -} - #[derive(Debug, PartialEq, Hash)] pub struct ContextId { // Which component is the scope in From 7102fe5f984fe8692cf977ea3a43e2973eed4f45 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Mon, 21 Jun 2021 01:35:12 -0400 Subject: [PATCH 05/20] docs: add some more sources in the core implementation --- packages/core/.vscode/spellright.dict | 2 + packages/core/Cargo.toml | 1 + packages/core/README.md | 67 +++++++++++++++------------ packages/core/src/diff.rs | 47 ++++++++++--------- 4 files changed, 66 insertions(+), 51 deletions(-) diff --git a/packages/core/.vscode/spellright.dict b/packages/core/.vscode/spellright.dict index c0214aff3..bee781ebd 100644 --- a/packages/core/.vscode/spellright.dict +++ b/packages/core/.vscode/spellright.dict @@ -4,3 +4,5 @@ dom virtualdom ns nohasher +Preact +vnodes diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index c7767b52b..c77791ede 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -33,6 +33,7 @@ log = "0.4" # Serialize the Edits for use in Webview/Liveview instances serde = { version="1", features=["derive"], optional=true } +smallvec = "1.6.1" [features] default = [] diff --git a/packages/core/README.md b/packages/core/README.md index c08206f0c..b88a595ec 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -12,6 +12,19 @@ Dioxus-core builds off the many frameworks that came before it. Notably, Dioxus - Dodrio: bump allocation, double buffering, and source code for NodeBuilder - Percy: html! macro architecture, platform-agnostic edits - Yew: passion and inspiration ❤️ +- InfernoJS: approach to fragments and node diffing +- Preact: approach for normalization and ref + +Dioxus-core leverages some really cool techniques and hits a very high level of parity with mature frameworks. Some unique features include: + +- managed lifetimes for borrowed data +- suspended nodes (task/fiber endpoints) for asynchronous vnodes +- custom memory allocator for vnodes and all text content +- support for fragments w/ lazy normalization + +There's certainly more to the story, but these optimizations make Dioxus memory use and allocation count extremely minimal. For an average application, it is likely that zero allocations will need to be performed once the app has been mounted. Only when new components are added to the dom will allocations occur - and only en mass. The space of old VNodes is dynamically recycled as new nodes are added. Additionally, Dioxus tracks the average memory footprint of previous components to estimate how much memory allocate for future components. + +All in all, Dioxus treats memory as an incredibly valuable resource. Combined with the memory-efficient footprint of WASM compilation, Dioxus apps can scale to thousands of components and still stay snappy and respect your RAM usage. ## Goals @@ -31,40 +44,36 @@ We have big goals for Dioxus. The final implementation must: - Support lazy VNodes (ie VNodes that are not actually created when the html! macro is used) - Support advanced diffing strategies (patience, Meyers, etc) -## Design Quirks - -- Use of "Context" as a way of mitigating threading issues and the borrow checker. (JS relies on globals) -- html! is lazy - needs to be used with a partner function to actually allocate the html. (Good be a good thing or a bad thing) - -```rust -let text = TextRenderer::render(html! {

"hello world"
}); -//
hello world
-``` - ```rust -fn main() { - tide::new() - .get("blah", serve_app("../")) - .get("blah", ws_handler(serve_app)) +rsx!{ "this is a text node" } + +rsx!{ + div {} + "asd" + div {} + div {} +} +rsx!{ + div { + a {} + b {} + c {} + Container { + Container { + Container { + Container { + Container { + div {} + } + } + } + } + } + } } -fn serve_app(ctx: &Context<()>) -> VNode { - let livecontext = LiveContext::new() - .with_handler("graph", graph_component) - .with_handler("graph", graph_component) - .with_handler("graph", graph_component) - .with_handler("graph", graph_component) - .with_handler("graph", graph_component) - .with_handler("graph", graph_component) - .build(); - ctx.render(html! { - - - - }) -} ``` diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 409ca6c84..d5aa86c65 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -95,14 +95,6 @@ pub struct DiffMachine<'a, Dom: RealDom> { pub seen_nodes: FxHashSet, } -// // todo: see if unsafe works better -// static COUNTER: Cell = Cell::new(1); -// fn next_id() -> u32 { -// let out = COUNTER.get(); -// COUNTER.set(out + 1); -// out -// } - impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { pub fn new( dom: &'a mut Dom, @@ -262,21 +254,32 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { } } - VNode::Fragment(_) => match new_node { - VNode::Fragment(_) => todo!(), - VNode::Element(_) => todo!(), - VNode::Text(_) => todo!(), - VNode::Suspended => todo!(), - VNode::Component(_) => todo!(), - }, + VNode::Fragment(old) => { + // + match new_node { + VNode::Fragment(_) => todo!(), - VNode::Suspended => match new_node { - VNode::Suspended => todo!(), - VNode::Element(_) => todo!(), - VNode::Text(_) => todo!(), - VNode::Fragment(_) => todo!(), - VNode::Component(_) => todo!(), - }, + // going from fragment to element means we're going from many (or potentially none) to one + VNode::Element(new) => {} + VNode::Text(_) => todo!(), + VNode::Suspended => todo!(), + VNode::Component(_) => todo!(), + } + } + + // a suspended node will perform a mem-copy of the previous elements until it is ready + // this means that event listeners will need to be disabled and removed + // it also means that props will need to disabled - IE if the node "came out of hibernation" any props should be considered outdated + VNode::Suspended => { + // + match new_node { + VNode::Suspended => todo!(), + VNode::Element(_) => todo!(), + VNode::Text(_) => todo!(), + VNode::Fragment(_) => todo!(), + VNode::Component(_) => todo!(), + } + } } } From 73047fe95678d50fcfd62a4ace7c6b406c5304e1 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 22 Jun 2021 17:20:54 -0400 Subject: [PATCH 06/20] feat: props memoization is more powerful This commit solves the memoization , properly memoizing properties that don't have any generic parameters. This is a rough heuristic to prevent non-static lifetimes from creeping into props and breaking our minual lifetime management. Props that have a generic parameter are opted-out of the `partialeq` requirement and props *without* lifetimes must implement partialeq. We're going to leave manual disabling of memoization for future work. --- Cargo.toml | 3 +- notes/TODO.md | 2 +- packages/cli/Cargo.toml | 4 +- packages/cli/src/builder.rs | 2 +- packages/core-macro/src/props/mod.rs | 19 +- packages/core/.vscode/settings.json | 2 +- packages/core/examples/borrowed.rs | 6 +- packages/core/src/component.rs | 27 +- packages/core/src/debug_renderer.rs | 24 +- packages/core/src/diff.rs | 38 +- packages/core/src/lib.rs | 6 +- packages/core/src/nodebuilder.rs | 12 + packages/core/src/nodes.rs | 243 ++++--- packages/core/src/patch.rs | 695 ------------------- packages/core/src/virtual_dom.rs | 17 +- packages/web/Cargo.toml | 6 +- packages/web/examples/basic.rs | 19 +- packages/web/examples/context.rs | 18 +- packages/web/examples/deep.rs | 1 + packages/web/examples/demoday.rs | 1 + packages/web/examples/derive.rs | 4 - packages/web/examples/events.rs | 22 +- packages/web/examples/framework_benchmark.rs | 184 +++++ packages/web/examples/hello.rs | 15 +- packages/web/examples/helloworld.rs | 1 + packages/web/examples/infer.rs | 9 +- packages/web/examples/input.rs | 21 +- packages/web/examples/landingpage.rs | 2 +- packages/web/examples/list.rs | 2 +- packages/web/examples/many.rs | 1 + packages/web/examples/props.rs | 13 + packages/web/examples/rsxt.rs | 31 +- packages/web/examples/todomvc/main.rs | 15 +- packages/web/examples/todomvc_simple.rs | 22 +- packages/web/examples/todomvcsingle.rs | 2 +- packages/web/src/lib.rs | 88 +-- packages/web/src/new.rs | 277 +++++++- 37 files changed, 877 insertions(+), 977 deletions(-) delete mode 100644 packages/core/src/patch.rs create mode 100644 packages/web/examples/framework_benchmark.rs create mode 100644 packages/web/examples/props.rs diff --git a/Cargo.toml b/Cargo.toml index cbba21a82..6dbd0ac8b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,7 +52,8 @@ members = [ "packages/core-macro", "packages/core", "packages/html-namespace", - # "packages/web", + "packages/web", + "packages/cli", # "packages/atoms", # "packages/ssr", # "packages/docsite", diff --git a/notes/TODO.md b/notes/TODO.md index 7ca8849ee..d1fff211a 100644 --- a/notes/TODO.md +++ b/notes/TODO.md @@ -2,7 +2,7 @@ - [] Transition away from names and towards compile-time safe tags - [] Fix diffing of fragments - [] Properly integrate memoization to prevent safety issues with children -- [] Understand the issue with callbacks (outdated generations) +- [x] Understand the issue with callbacks (outdated generations) - [] Fix examples for core, web, ssr, and general - [] Finish up documentation - [] Polish the Recoil (Dirac?) API diff --git a/packages/cli/Cargo.toml b/packages/cli/Cargo.toml index c5a88c46f..bfc4a3622 100644 --- a/packages/cli/Cargo.toml +++ b/packages/cli/Cargo.toml @@ -11,13 +11,13 @@ description = "CLI tool for developing, testing, and publishing Dioxus apps" [dependencies] thiserror = "1.0.23" log = "0.4.13" -fern = { version = "0.6.0", features = ["colored"] } +fern = { version="0.6.0", features=["colored"] } wasm-bindgen-cli-support = "0.2.73" anyhow = "1.0.38" argh = "0.1.4" serde = "1.0.120" serde_json = "1.0.61" -async-std = { version = "1.9.0", features = ["attributes"] } +async-std = { version="1.9.0", features=["attributes"] } tide = "0.15.0" fs_extra = "1.2.0" diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 3cecf4e35..bf3f85254 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -131,7 +131,7 @@ fn gen_page(module: &str) -> String { diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 0301c68bd..9291dd6de 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -502,6 +502,7 @@ mod field_info { mod struct_info { use proc_macro2::TokenStream; + use quote::__private::ext::RepToTokensExt; use quote::quote; use syn::parse::Error; @@ -569,6 +570,13 @@ mod struct_info { ref builder_name, .. } = *self; + + // we're generating stuff that goes into unsafe code here + // we use the heuristic: are there *any* generic parameters? + // If so, then they might have non-static lifetimes and we can't compare two generic things that *might borrow* + // Therefore, we will generate code that shortcircuits the "comparison" in memoization + let are_there_generics = self.generics.params.len() > 0; + let (impl_generics, ty_generics, where_clause) = self.generics.split_for_impl(); let all_fields_param = syn::GenericParam::Type( syn::Ident::new("TypedBuilderFields", proc_macro2::Span::call_site()).into(), @@ -650,6 +658,11 @@ Finally, call `.build()` to create the instance of `{name}`. .extend(predicates.predicates.clone()); } + let can_memoize = match are_there_generics { + true => quote! { false }, + false => quote! { self == other }, + }; + Ok(quote! { impl #impl_generics #name #ty_generics #where_clause { #[doc = #builder_method_doc] @@ -679,12 +692,14 @@ Finally, call `.build()` to create the instance of `{name}`. } } - unsafe impl #impl_generics dioxus::prelude::Properties for #name #ty_generics{ + impl #impl_generics dioxus::prelude::Properties for #name #ty_generics{ type Builder = #builder_name #generics_with_empty; - const CAN_BE_MEMOIZED: bool = true; fn builder() -> Self::Builder { #name::builder() } + unsafe fn memoize(&self, other: &Self) -> bool { + #can_memoize + } } }) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 2f418701b..80f51cff8 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": true + "rust-analyzer.inlayHints.enable": false } \ No newline at end of file diff --git a/packages/core/examples/borrowed.rs b/packages/core/examples/borrowed.rs index 25615f08a..df57c3819 100644 --- a/packages/core/examples/borrowed.rs +++ b/packages/core/examples/borrowed.rs @@ -63,10 +63,12 @@ impl PartialEq for ChildProps { false } } -unsafe impl Properties for ChildProps { +impl Properties for ChildProps { type Builder = (); - const CAN_BE_MEMOIZED: bool = false; fn builder() -> Self::Builder { () } + unsafe fn memoize(&self, other: &Self) -> bool { + self == other + } } diff --git a/packages/core/src/component.rs b/packages/core/src/component.rs index 8329fb606..77ae04fc3 100644 --- a/packages/core/src/component.rs +++ b/packages/core/src/component.rs @@ -7,21 +7,27 @@ use crate::innerlude::FC; -pub unsafe trait Properties: PartialEq + Sized { +pub trait Properties: Sized { type Builder; - const CAN_BE_MEMOIZED: bool; fn builder() -> Self::Builder; + + /// Memoization can only happen if the props are 'static + /// The user must know if their props are static, but if they make a mistake, UB happens + /// Therefore it's unsafe to memeoize. + unsafe fn memoize(&self, other: &Self) -> bool; } -unsafe impl Properties for () { - const CAN_BE_MEMOIZED: bool = true; +impl Properties for () { type Builder = EmptyBuilder; - fn builder() -> Self::Builder { EmptyBuilder {} } + unsafe fn memoize(&self, _other: &Self) -> bool { + true + } } - +// We allow components to use the () generic parameter if they have no props. This impl enables the "build" method +// that the macros use to anonymously complete prop construction. pub struct EmptyBuilder; impl EmptyBuilder { #[inline] @@ -30,6 +36,8 @@ impl EmptyBuilder { } } +/// This utility function launches the builder method so rsx! and html! macros can use the typed-builder pattern +/// to initialize a component's props. pub fn fc_to_builder(_: FC) -> T::Builder { T::builder() } @@ -39,9 +47,10 @@ pub fn fc_to_builder(_: FC) -> T::Builder { /// /// Fragments capture a series of children without rendering extra nodes. /// -/// -/// -pub static Fragment: FC<()> = |ctx| { +/// Fragments are incredibly useful when necessary, but *do* add cost in the diffing phase. +/// Try to avoid nesting fragments if you can. Infinitely nested Fragments *will* cause diffing to crash. +#[allow(non_upper_case_globals)] +pub const Fragment: FC<()> = |ctx| { use crate::prelude::*; ctx.render(LazyNodes::new(move |c| { crate::nodebuilder::vfragment(c, None, ctx.children()) diff --git a/packages/core/src/debug_renderer.rs b/packages/core/src/debug_renderer.rs index 382c97fc2..605758bc9 100644 --- a/packages/core/src/debug_renderer.rs +++ b/packages/core/src/debug_renderer.rs @@ -3,6 +3,7 @@ //! //! Renderers don't actually need to own the virtual dom (it's up to the implementer). +use crate::innerlude::RealDom; use crate::{events::EventTrigger, virtual_dom::VirtualDom}; use crate::{innerlude::Result, prelude::*}; @@ -40,7 +41,7 @@ impl DebugRenderer { Ok(()) } - pub fn step(&mut self, machine: &mut DiffMachine) -> Result<()> { + pub fn step(&mut self, machine: &mut DiffMachine) -> Result<()> { Ok(()) } @@ -70,6 +71,27 @@ impl DebugRenderer { pub fn trigger_listener(&mut self, id: usize) -> Result<()> { Ok(()) } + + pub fn render_nodes<'a, F>(&self, other: LazyNodes<'a, F>) -> Result<()> + where + F: for<'b> FnOnce(&'b NodeCtx<'a>) -> VNode<'a> + 'a, + { + Ok(()) + } +} + +pub struct DebugVNodeSource { + bump: Bump, +} +impl DebugVNodeSource { + fn new() -> Self { + Self { bump: Bump::new() } + } + + fn render_nodes(&self) -> VNode { + // let ctx = NodeCtx + todo!() + } } #[cfg(test)] diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index d5aa86c65..f235c4535 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -44,31 +44,30 @@ use std::{ /// single node pub trait RealDom { // Navigation - fn push_root(&self, root: RealDomNode); - fn pop(&self); + fn push_root(&mut self, root: RealDomNode); // Add Nodes to the dom - fn append_child(&self); - fn replace_with(&self); + fn append_child(&mut self); + fn replace_with(&mut self); // Remove Nodesfrom the dom - fn remove(&self); - fn remove_all_children(&self); + fn remove(&mut self); + fn remove_all_children(&mut self); // Create - fn create_text_node(&self, text: &str) -> RealDomNode; - fn create_element(&self, tag: &str) -> RealDomNode; - fn create_element_ns(&self, tag: &str, namespace: &str) -> RealDomNode; + fn create_text_node(&mut self, text: &str) -> RealDomNode; + fn create_element(&mut self, tag: &str) -> RealDomNode; + fn create_element_ns(&mut self, tag: &str, namespace: &str) -> RealDomNode; // events - fn new_event_listener(&self, event: &str, scope: ScopeIdx, id: usize); - // fn new_event_listener(&self, event: &str); - fn remove_event_listener(&self, event: &str); + fn new_event_listener(&mut self, event: &str, scope: ScopeIdx, id: usize); + // fn new_event_listener(&mut self, event: &str); + fn remove_event_listener(&mut self, event: &str); // modify - fn set_text(&self, text: &str); - fn set_attribute(&self, name: &str, value: &str, is_namespaced: bool); - fn remove_attribute(&self, name: &str); + fn set_text(&mut self, text: &str); + fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool); + fn remove_attribute(&mut self, name: &str); // node ref fn raw_node_as_any_mut(&self) -> &mut dyn Any; @@ -470,7 +469,7 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // [... node] // // The change list stack is left unchanged. - fn diff_listeners(&self, old: &[Listener<'_>], new: &[Listener<'_>]) { + fn diff_listeners(&mut self, old: &[Listener<'_>], new: &[Listener<'_>]) { if !old.is_empty() || !new.is_empty() { // self.dom.commit_traversal(); } @@ -518,7 +517,12 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // [... node] // // The change list stack is left unchanged. - fn diff_attr(&self, old: &'a [Attribute<'a>], new: &'a [Attribute<'a>], is_namespaced: bool) { + fn diff_attr( + &mut self, + old: &'a [Attribute<'a>], + new: &'a [Attribute<'a>], + is_namespaced: bool, + ) { // Do O(n^2) passes to add/update and remove attributes, since // there are almost always very few attributes. // diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 61cd18494..987934316 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -11,10 +11,9 @@ pub mod arena; pub mod component; // Logic for extending FC -// pub mod debug_renderer; +pub mod debug_renderer; pub mod diff; -pub mod patch; // An "edit phase" described by transitions and edit operations // Test harness for validating that lifecycles and diffs work appropriately - // the diffing algorithm that builds the ChangeList + pub mod error; // Error type we expose to the renderers pub mod events; // Manages the synthetic event API pub mod hooks; // Built-in hooks @@ -36,7 +35,6 @@ pub(crate) mod innerlude { pub use crate::hooks::*; pub use crate::nodebuilder::*; pub use crate::nodes::*; - pub use crate::patch::*; pub use crate::virtual_dom::*; pub type FC

= fn(Context

) -> VNode; diff --git a/packages/core/src/nodebuilder.rs b/packages/core/src/nodebuilder.rs index cc60428ca..ff8f04c4c 100644 --- a/packages/core/src/nodebuilder.rs +++ b/packages/core/src/nodebuilder.rs @@ -494,10 +494,22 @@ where /// .finish(); /// ``` pub fn iter_child(mut self, nodes: impl IntoIterator>) -> Self { + let len_before = self.children.len(); for item in nodes { let child = item.into_vnode(&self.ctx); self.children.push(child); } + let len_after = self.children.len(); + if len_after > len_before { + let last_child = self.children.last().unwrap(); + if last_child.key().is_none() { + // TODO: Somehow get the name of the component when NodeCtx is being made + const ERR_MSG: &str = r#"Warning: Each child in an array or iterator should have a unique "key" prop. + Check the render method of XXXX. + See fb.me/react-warning-keys for more information. "#; + log::error!("{}", ERR_MSG); + } + } self } } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 0fdb6eef5..cf245785d 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -111,9 +111,29 @@ impl<'a> VNode<'a> { } } - fn get_child(&self, id: u32) -> Option> { + fn get_child(&self, id: u32) -> Option<&'a VNode<'a>> { todo!() } + + pub fn is_real(&self) -> bool { + match self { + VNode::Element(_) => true, + VNode::Text(_) => true, + VNode::Fragment(_) => false, + VNode::Suspended => false, + VNode::Component(_) => false, + } + } + + pub fn get_mounted_id(&self) -> Option { + match self { + VNode::Element(_) => todo!(), + VNode::Text(_) => todo!(), + VNode::Fragment(_) => todo!(), + VNode::Suspended => todo!(), + VNode::Component(_) => todo!(), + } + } } #[derive(Clone)] @@ -257,88 +277,68 @@ pub struct VComponent<'src> { } impl<'a> VComponent<'a> { - // use the type parameter on props creation and move it into a portable context - // this lets us keep scope generic *and* downcast its props when we need to: - // - perform comparisons when diffing (memoization) - // TODO: lift the requirement that props need to be static - // we want them to borrow references... maybe force implementing a "to_static_unsafe" trait - + /// When the rsx! macro is called, it will check if the CanMemo flag is set to true (from the Props impl) + /// If it is set to true, then this method will be called which implements automatic memoization. + /// + /// If the CanMemo is `false`, then the macro will call the backup method which always defaults to "false" pub fn new( - // bump: &'a Bump, ctx: &NodeCtx<'a>, component: FC

, - // props: bumpalo::boxed::Box<'a, P>, props: P, key: Option<&'a str>, children: &'a [VNode<'a>], ) -> Self { - // pub fn new(component: FC

, props: P, key: Option<&'a str>) -> Self { - // let bad_props = unsafe { transmogrify(props) }; let bump = ctx.bump(); - let caller_ref = component as *const (); - let props = bump.alloc(props); + let user_fc = component as *const (); + let props = bump.alloc(props); let raw_props = props as *const P as *const (); let comparator: Option<&dyn Fn(&VComponent) -> bool> = { - if P::CAN_BE_MEMOIZED { - Some(bump.alloc(move |other: &VComponent| { - // Safety: - // We are guaranteed that the props will be of the same type because - // there is no way to create a VComponent other than this `new` method. - // - // Therefore, if the render functions are identical (by address), then so will be - // props type paramter (because it is the same render function). Therefore, we can be - // sure - if caller_ref == other.user_fc { - // let g = other.raw_ctx.downcast_ref::

().unwrap(); - let real_other = unsafe { &*(other.raw_props as *const _ as *const P) }; - &props == &real_other - } else { - false + Some(bump.alloc(move |other: &VComponent| { + // Safety: + // ------ + // + // Invariants: + // - Component function pointers are the same + // - Generic properties on the same function pointer are the same + // - Lifetime of P borrows from its parent + // - The parent scope still exists when method is called + // - Casting from T to *const () is portable + // + // Explanation: + // We are guaranteed that the props will be of the same type because + // there is no way to create a VComponent other than this `new` method. + // + // Therefore, if the render functions are identical (by address), then so will be + // props type paramter (because it is the same render function). Therefore, we can be + // sure that it is safe to interperet the previous props raw pointer as the same props + // type. From there, we can call the props' "memoize" method to see if we can + // avoid re-rendering the component. + if user_fc == other.user_fc { + let real_other = unsafe { &*(other.raw_props as *const _ as *const P) }; + let props_memoized = unsafe { props.memoize(&real_other) }; + match (props_memoized, children.len() == 0) { + (true, true) => true, + _ => false, } - })) - } else { - None - } + } else { + false + } + })) }; - // let prref: &'a P = props.as_ref(); - - // let r = create_closure(component, raw_props); - // let caller: Rc Fn(&'g Scope) -> VNode<'g>> = Rc::new(move |scope| { - // // r(scope); - // // - // // let props2 = bad_props; - // // props.as_ref(); - // // let ctx = Context { - // // props: prref, - // // scope, - // // }; - // // let ctx: Context<'g, P> = todo!(); - // // todo!() - // // let r = component(ctx); - // todo!() - // }); - let caller = create_closure(component, raw_props); - - // let caller: Rc VNode> = Rc::new(create_closure(component, raw_props)); - - let key = match key { - Some(key) => NodeKey::new(key), - None => NodeKey(None), - }; - - // raw_props: Box::new(props), - // comparator: Rc::new(props_comparator), Self { - key, - ass_scope: RefCell::new(None), - user_fc: caller_ref, + user_fc, comparator, raw_props, children, - caller, + ass_scope: RefCell::new(None), + key: match key { + Some(key) => NodeKey::new(key), + None => NodeKey(None), + }, + caller: create_closure(component, raw_props), mounted_root: Cell::new(RealDomNode::empty()), } } @@ -346,7 +346,7 @@ impl<'a> VComponent<'a> { type Captured<'a> = Rc Fn(&'r Scope) -> VNode<'r> + 'a>; -fn create_closure<'a, P: Properties + 'a>( +fn create_closure<'a, P: 'a>( component: FC

, raw_props: *const (), ) -> Rc Fn(&'r Scope) -> VNode<'r>> { @@ -385,7 +385,9 @@ impl<'a> VFragment<'a> { } /// This method converts a list of nested real/virtual nodes into a stream of nodes that are definitely associated -/// with the real dom. +/// with the real dom. The only types of nodes that may be returned are text, elemets, and components. +/// +/// Components *are* considered virtual, but this iterator can't necessarily handle them without the scope arena. /// /// Why? /// --- @@ -401,47 +403,80 @@ pub fn iterate_real_nodes<'a>(nodes: &'a [VNode<'a>]) -> RealNodeIterator<'a> { RealNodeIterator::new(nodes) } -struct RealNodeIterator<'a> { +pub struct RealNodeIterator<'a> { nodes: &'a [VNode<'a>], - // an idx for each level of nesting - // it's highly highly unlikely to hit 4 levels of nested fragments - // so... we just don't support it - nesting_idxs: [Option; 3], + // this node is always a "real" node + // the index is "what sibling # is it" + // IE in a list of children on a fragment, the node will be a text node that's the 5th sibling + node_stack: Vec<(&'a VNode<'a>, u32)>, } impl<'a> RealNodeIterator<'a> { + // We immediately descend to the first real node we can find fn new(nodes: &'a [VNode<'a>]) -> Self { - Self { - nodes, - nesting_idxs: [None, None, None], + let mut node_stack = Vec::new(); + if nodes.len() > 0 { + let mut cur_node = nodes.get(0).unwrap(); + loop { + node_stack.push((cur_node, 0_u32)); + if !cur_node.is_real() { + cur_node = cur_node.get_child(0).unwrap(); + } else { + break; + } + } } + + Self { nodes, node_stack } } - // advances the cursor to the next element, panicing if we're on the 3rd level and still finding fragments - fn advance_cursor(&mut self) { - match self.nesting_idxs { - [None, ..] => {} + // // advances the cursor to the next element, panicing if we're on the 3rd level and still finding fragments + // fn advance_cursor(&mut self) { + // let (mut cur_node, mut cur_id) = self.node_stack.last().unwrap(); + + // while !cur_node.is_real() { + // match cur_node { + // VNode::Element(_) | VNode::Text(_) => todo!(), + // VNode::Suspended => todo!(), + // VNode::Component(_) => todo!(), + // VNode::Fragment(frag) => { + // let p = frag.children; + // } + // } + // } + // } + + fn next_node(&mut self) -> bool { + let (mut cur_node, cur_id) = self.node_stack.last_mut().unwrap(); + + match cur_node { + VNode::Fragment(frag) => { + // + if *cur_id + 1 > frag.children.len() as u32 { + self.node_stack.pop(); + let next = self.node_stack.last_mut(); + return false; + } + *cur_id += 1; + true + } + + VNode::Element(_) => todo!(), + VNode::Text(_) => todo!(), + VNode::Suspended => todo!(), + VNode::Component(_) => todo!(), } } fn get_current_node(&self) -> Option<&VNode<'a>> { - match self.nesting_idxs { - [None, None, None] => None, - [Some(a), None, None] => Some(&self.nodes[a as usize]), - [Some(a), Some(b), None] => { - // - *&self.nodes[a as usize].get_child(b).as_ref() - } - [Some(a), Some(b), Some(c)] => { - // - *&self.nodes[a as usize] - .get_child(b) - .unwrap() - .get_child(c) - .as_ref() - } - } + self.node_stack.last().map(|(node, id)| match node { + VNode::Element(_) => todo!(), + VNode::Text(_) => todo!(), + VNode::Fragment(_) => todo!(), + VNode::Suspended => todo!(), + VNode::Component(_) => todo!(), + }) } } @@ -485,12 +520,26 @@ impl<'a> Iterator for RealNodeIterator<'a> { } mod tests { + use crate::debug_renderer::DebugRenderer; use crate::nodebuilder::LazyNodes; + use crate as dioxus; + use dioxus::prelude::*; #[test] fn iterate_nodes() { - // let t1 = LazyNodes::new(|b| { - // // - // }); + let rs = rsx! { + Fragment { + Fragment { + Fragment { + Fragment { + h1 {"abc1"} + } + h2 {"abc2"} + } + h3 {"abc3"} + } + h4 {"abc4"} + } + }; } } diff --git a/packages/core/src/patch.rs b/packages/core/src/patch.rs deleted file mode 100644 index 0995c4ba2..000000000 --- a/packages/core/src/patch.rs +++ /dev/null @@ -1,695 +0,0 @@ -//! Changelist -//! ---------- -//! -//! This module exposes the "changelist" object which allows 3rd party implementors to handle diffs to the virtual dom. -//! -//! # Design -//! --- -//! In essence, the changelist object connects a diff of two vdoms to the actual edits required to update the output renderer. -//! -//! This abstraction relies on the assumption that the final renderer accepts a tree of elements. For most target platforms, -//! this is an appropriate abstraction . -//! -//! During the diff phase, the change list is built. Once the diff phase is over, the change list is finished and returned back -//! to the renderer. The renderer is responsible for propogating the updates to the final display. -//! -//! Because the change list references data internal to the vdom, it needs to be consumed by the renderer before the vdom -//! can continue to work. This means once a change list is generated, it should be consumed as fast as possible, otherwise the -//! dom will be blocked from progressing. This is enforced by lifetimes on the returend changelist object. -//! -//! # Known Issues -//! ---- -//! - stack machine approach does not work when 3rd party extensions inject elements (breaking our view of the dom) - solvable by the renderer - -use crate::innerlude::ScopeIdx; - -pub type EditList<'src> = Vec>; - -/// The `Edit` represents a single modifcation of the renderer tree. -/// todo @jon, go through and make certain fields static. tag names should be known at compile time -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "serde", serde(tag = "type"))] -#[derive(Debug)] -pub enum Edit<'src_bump> { - // ======================================================== - // Common Ops: The most common operation types - // ======================================================== - SetText { - text: &'src_bump str, - }, - SetClass { - class_name: &'src_bump str, - }, - CreateTextNode { - text: &'src_bump str, - }, - CreateElement { - // todo - make static? - tag_name: &'src_bump str, - }, - CreateElementNs { - // todo - make static? - tag_name: &'src_bump str, - // todo - make static? - ns: &'src_bump str, - }, - - // ======================================================== - // Attributes - // ======================================================== - SetAttribute { - name: &'src_bump str, - value: &'src_bump str, - }, - RemoveAttribute { - name: &'src_bump str, - }, - RemoveChild { - n: u32, - }, - - // ============================================================ - // Event Listeners: Event types and IDs used to update the VDOM - // ============================================================ - NewListener { - // todo - make static? - event: &'src_bump str, - scope: ScopeIdx, - id: usize, - }, - UpdateListener { - // todo - make static? - event: &'src_bump str, - scope: ScopeIdx, - id: usize, - }, - RemoveListener { - // todo - make static? - event: &'src_bump str, - }, - - // ======================================================== - // Cached Roots: The mount point for individual components - // Allows quick traversal to cached entrypoints - // ======================================================== - // push a known node on to the stack - TraverseToKnown { - node: u32, - // node: ScopeIdx, - }, - // Add the current top of the stack to the known nodes - MakeKnown { - node: u32, - // node: ScopeIdx, - }, - // Remove the current top of the stack from the known nodes - RemoveKnown, - - // ======================================================== - // Stack OPs: Operations for manipulating the stack machine - // ======================================================== - PushReverseChild { - n: u32, - }, - PopPushChild { - n: u32, - }, - Pop, - AppendChild, - RemoveSelfAndNextSiblings {}, - ReplaceWith, - SaveChildrenToTemporaries { - temp: u32, - start: u32, - end: u32, - }, - PushChild { - n: u32, - }, - PushTemporary { - temp: u32, - }, - InsertBefore, - PopPushReverseChild { - n: u32, - }, -} - -/// The edit machine represents a stream of differences between two component trees. -/// -/// This struct is interesting in that it keeps track of differences by borrowing -/// from the source rather than writing to a new buffer. This means that the virtual dom -/// *cannot* be updated while this machine is in existence without "unsafe". -/// -/// This unsafety is handled by methods on the virtual dom and is not exposed via lib code. -pub struct EditMachine<'lock> { - pub traversal: Traversal, - next_temporary: u32, - forcing_new_listeners: bool, - pub cur_height: u32, - - // // if the current node is a "known" node - // // any actions that modify this node should update the mapping - // current_known: Option, - pub emitter: EditList<'lock>, -} - -impl<'lock> EditMachine<'lock> { - pub fn new() -> Self { - Self { - // current_known: None, - traversal: Traversal::new(), - cur_height: 0, - next_temporary: 0, - forcing_new_listeners: false, - emitter: EditList::<'lock>::default(), - } - } -} - -// =================================== -// Traversal Methods -// =================================== -impl<'src> EditMachine<'src> { - pub fn go_down(&mut self) { - self.traversal.down(); - } - - pub fn go_down_to_child(&mut self, index: usize) { - self.traversal.down(); - self.traversal.sibling(index); - } - - pub fn go_down_to_reverse_child(&mut self, index: usize) { - self.traversal.down(); - self.traversal.reverse_sibling(index); - } - - pub fn go_up(&mut self) { - self.traversal.up(); - } - - pub fn go_to_sibling(&mut self, index: usize) { - self.traversal.sibling(index); - } - - pub fn go_to_temp_sibling(&mut self, temp: u32) { - self.traversal.up(); - self.traversal.down_to_temp(temp); - } - - pub fn go_down_to_temp_child(&mut self, temp: u32) { - self.traversal.down_to_temp(temp); - } - - pub fn commit_traversal(&mut self) { - if self.traversal.is_committed() { - return; - } - - for mv in self.traversal.commit() { - match mv { - MoveTo::Parent => self.emitter.push(Edit::Pop {}), - MoveTo::Child(n) => self.emitter.push(Edit::PushChild { n }), - MoveTo::ReverseChild(n) => self.emitter.push(Edit::PushReverseChild { n }), - MoveTo::Sibling(n) => self.emitter.push(Edit::PopPushChild { n }), - MoveTo::ReverseSibling(n) => self.emitter.push(Edit::PopPushReverseChild { n }), - MoveTo::TempChild(temp) => self.emitter.push(Edit::PushTemporary { temp }), - } - } - } - - pub fn traversal_is_committed(&self) -> bool { - self.traversal.is_committed() - } -} - -// =================================== -// Stack methods -// =================================== -impl<'a> EditMachine<'a> { - pub fn next_temporary(&self) -> u32 { - self.next_temporary - } - - pub fn set_next_temporary(&mut self, next_temporary: u32) { - self.next_temporary = next_temporary; - } - - pub fn save_children_to_temporaries(&mut self, start: usize, end: usize) -> u32 { - debug_assert!(self.traversal_is_committed()); - debug_assert!(start < end); - let temp_base = self.next_temporary; - self.next_temporary = temp_base + (end - start) as u32; - self.emitter.push(Edit::SaveChildrenToTemporaries { - temp: temp_base, - start: start as u32, - end: end as u32, - }); - temp_base - } - - pub fn push_temporary(&mut self, temp: u32) { - debug_assert!(self.traversal_is_committed()); - - self.emitter.push(Edit::PushTemporary { temp }); - } - - pub fn remove_child(&mut self, child: usize) { - debug_assert!(self.traversal_is_committed()); - - self.emitter.push(Edit::RemoveChild { n: child as u32 }) - } - - pub fn insert_before(&mut self) { - debug_assert!(self.traversal_is_committed()); - - self.emitter.push(Edit::InsertBefore {}) - } - - pub fn set_text(&mut self, text: &'a str) { - debug_assert!(self.traversal_is_committed()); - self.emitter.push(Edit::SetText { text }); - } - - pub fn remove_self_and_next_siblings(&mut self) { - debug_assert!(self.traversal_is_committed()); - self.emitter.push(Edit::RemoveSelfAndNextSiblings {}); - } - - pub fn replace_with(&mut self) { - debug_assert!(self.traversal_is_committed()); - // debug!("emit: replace_with()"); - // if let Some(id) = self.current_known { - // // update mapping - // self.emitter.push(Edit::MakeKnown{node: id}); - // self.current_known = None; - // } - // self.emitter.replace_with(); - self.emitter.push(Edit::ReplaceWith {}); - } - - pub fn set_attribute(&mut self, name: &'a str, value: &'a str, is_namespaced: bool) { - debug_assert!(self.traversal_is_committed()); - - if name == "class" && !is_namespaced { - self.emitter.push(Edit::SetClass { class_name: value }); - } else { - self.emitter.push(Edit::SetAttribute { name, value }); - } - } - - pub fn remove_attribute(&mut self, name: &'a str) { - self.emitter.push(Edit::RemoveAttribute { name }); - } - - pub fn append_child(&mut self) { - debug_assert!(self.traversal_is_committed()); - self.emitter.push(Edit::AppendChild {}); - } - - pub fn create_text_node(&mut self, text: &'a str) { - debug_assert!(self.traversal_is_committed()); - self.emitter.push(Edit::CreateTextNode { text }); - } - - pub fn create_element(&mut self, tag_name: &'a str) { - debug_assert!(self.traversal_is_committed()); - self.emitter.push(Edit::CreateElement { tag_name }); - } - - pub fn create_element_ns(&mut self, tag_name: &'a str, ns: &'a str) { - debug_assert!(self.traversal_is_committed()); - self.emitter.push(Edit::CreateElementNs { tag_name, ns }); - } - - pub fn push_force_new_listeners(&mut self) -> bool { - let old = self.forcing_new_listeners; - self.forcing_new_listeners = true; - old - } - - pub fn pop_force_new_listeners(&mut self, previous: bool) { - debug_assert!(self.forcing_new_listeners); - self.forcing_new_listeners = previous; - } - - pub fn new_event_listener(&mut self, event: &'a str, scope: ScopeIdx, id: usize) { - debug_assert!(self.traversal_is_committed()); - self.emitter.push(Edit::NewListener { event, scope, id }); - } - - pub fn update_event_listener(&mut self, event: &'a str, scope: ScopeIdx, id: usize) { - debug_assert!(self.traversal_is_committed()); - if self.forcing_new_listeners { - self.new_event_listener(event, scope, id); - return; - } - - self.emitter.push(Edit::NewListener { event, scope, id }); - } - - pub fn remove_event_listener(&mut self, event: &'a str) { - debug_assert!(self.traversal_is_committed()); - self.emitter.push(Edit::RemoveListener { event }); - } - - pub fn save_known_root(&mut self, id: u32) { - log::debug!("emit: save_known_root({:?})", id); - self.emitter.push(Edit::MakeKnown { node: id }) - } - - pub fn load_known_root(&mut self, id: u32) { - log::debug!("emit: TraverseToKnown({:?})", id); - self.emitter.push(Edit::TraverseToKnown { node: id }) - } -} - -// Keeps track of where we are moving in a DOM tree, and shortens traversal -// paths between mutations to their minimal number of operations. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum MoveTo { - /// Move from the current node up to its parent. - Parent, - - /// Move to the current node's n^th child. - Child(u32), - - /// Move to the current node's n^th from last child. - ReverseChild(u32), - - /// Move to the n^th sibling. Not relative from the current - /// location. Absolute indexed within all of the current siblings. - Sibling(u32), - - /// Move to the n^th from last sibling. Not relative from the current - /// location. Absolute indexed within all of the current siblings. - ReverseSibling(u32), - - /// Move down to the given saved temporary child. - TempChild(u32), -} - -#[derive(Debug)] -pub struct Traversal { - uncommitted: Vec, -} - -impl Traversal { - /// Construct a new `Traversal` with its internal storage backed by the - /// given bump arena. - pub fn new() -> Traversal { - Traversal { - uncommitted: Vec::with_capacity(32), - } - } - - /// Move the traversal up in the tree. - pub fn up(&mut self) { - match self.uncommitted.last() { - Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => { - self.uncommitted.pop(); - self.uncommitted.push(MoveTo::Parent); - } - Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) | Some(MoveTo::ReverseChild(_)) => { - self.uncommitted.pop(); - // And we're back at the parent. - } - _ => { - self.uncommitted.push(MoveTo::Parent); - } - } - } - - /// Move the traversal down in the tree to the first child of the current - /// node. - pub fn down(&mut self) { - if let Some(&MoveTo::Parent) = self.uncommitted.last() { - self.uncommitted.pop(); - self.sibling(0); - } else { - self.uncommitted.push(MoveTo::Child(0)); - } - } - - /// Move the traversal to the n^th sibling. - pub fn sibling(&mut self, index: usize) { - let index = index as u32; - match self.uncommitted.last_mut() { - Some(MoveTo::Sibling(ref mut n)) | Some(MoveTo::Child(ref mut n)) => { - *n = index; - } - Some(MoveTo::ReverseSibling(_)) => { - self.uncommitted.pop(); - self.uncommitted.push(MoveTo::Sibling(index)); - } - Some(MoveTo::TempChild(_)) | Some(MoveTo::ReverseChild(_)) => { - self.uncommitted.pop(); - self.uncommitted.push(MoveTo::Child(index)) - } - _ => { - self.uncommitted.push(MoveTo::Sibling(index)); - } - } - } - - /// Move the the n^th from last sibling. - pub fn reverse_sibling(&mut self, index: usize) { - let index = index as u32; - match self.uncommitted.last_mut() { - Some(MoveTo::ReverseSibling(ref mut n)) | Some(MoveTo::ReverseChild(ref mut n)) => { - *n = index; - } - Some(MoveTo::Sibling(_)) => { - self.uncommitted.pop(); - self.uncommitted.push(MoveTo::ReverseSibling(index)); - } - Some(MoveTo::TempChild(_)) | Some(MoveTo::Child(_)) => { - self.uncommitted.pop(); - self.uncommitted.push(MoveTo::ReverseChild(index)) - } - _ => { - self.uncommitted.push(MoveTo::ReverseSibling(index)); - } - } - } - - /// Go to the given saved temporary. - pub fn down_to_temp(&mut self, temp: u32) { - match self.uncommitted.last() { - Some(MoveTo::Sibling(_)) | Some(MoveTo::ReverseSibling(_)) => { - self.uncommitted.pop(); - } - Some(MoveTo::Parent) - | Some(MoveTo::TempChild(_)) - | Some(MoveTo::Child(_)) - | Some(MoveTo::ReverseChild(_)) - | None => { - // Can't remove moves to parents since we rely on their stack - // pops. - } - } - self.uncommitted.push(MoveTo::TempChild(temp)); - } - - /// Are all the traversal's moves committed? That is, are there no moves - /// that have *not* been committed yet? - #[inline] - pub fn is_committed(&self) -> bool { - self.uncommitted.is_empty() - } - - /// Commit this traversals moves and return the optimized path from the last - /// commit. - #[inline] - pub fn commit(&mut self) -> std::vec::Drain<'_, MoveTo> { - self.uncommitted.drain(..) - } - - #[inline] - pub fn reset(&mut self) { - self.uncommitted.clear(); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_traversal() { - fn t(f: F) -> Box - where - F: 'static + FnMut(&mut Traversal), - { - Box::new(f) as _ - } - - for (mut traverse, expected_moves) in vec![ - ( - t(|t| { - t.down(); - }), - vec![MoveTo::Child(0)], - ), - ( - t(|t| { - t.up(); - }), - vec![MoveTo::Parent], - ), - ( - t(|t| { - t.sibling(42); - }), - vec![MoveTo::Sibling(42)], - ), - ( - t(|t| { - t.down(); - t.up(); - }), - vec![], - ), - ( - t(|t| { - t.down(); - t.sibling(2); - t.up(); - }), - vec![], - ), - ( - t(|t| { - t.down(); - t.sibling(3); - }), - vec![MoveTo::Child(3)], - ), - ( - t(|t| { - t.down(); - t.sibling(4); - t.sibling(8); - }), - vec![MoveTo::Child(8)], - ), - ( - t(|t| { - t.sibling(1); - t.sibling(1); - }), - vec![MoveTo::Sibling(1)], - ), - ( - t(|t| { - t.reverse_sibling(3); - }), - vec![MoveTo::ReverseSibling(3)], - ), - ( - t(|t| { - t.down(); - t.reverse_sibling(3); - }), - vec![MoveTo::ReverseChild(3)], - ), - ( - t(|t| { - t.down(); - t.reverse_sibling(3); - t.up(); - }), - vec![], - ), - ( - t(|t| { - t.down(); - t.reverse_sibling(3); - t.reverse_sibling(6); - }), - vec![MoveTo::ReverseChild(6)], - ), - ( - t(|t| { - t.up(); - t.reverse_sibling(3); - t.reverse_sibling(6); - }), - vec![MoveTo::Parent, MoveTo::ReverseSibling(6)], - ), - ( - t(|t| { - t.up(); - t.sibling(3); - t.sibling(6); - }), - vec![MoveTo::Parent, MoveTo::Sibling(6)], - ), - ( - t(|t| { - t.sibling(3); - t.sibling(6); - t.up(); - }), - vec![MoveTo::Parent], - ), - ( - t(|t| { - t.reverse_sibling(3); - t.reverse_sibling(6); - t.up(); - }), - vec![MoveTo::Parent], - ), - ( - t(|t| { - t.down(); - t.down_to_temp(3); - }), - vec![MoveTo::Child(0), MoveTo::TempChild(3)], - ), - ( - t(|t| { - t.down_to_temp(3); - t.sibling(5); - }), - vec![MoveTo::Child(5)], - ), - ( - t(|t| { - t.down_to_temp(3); - t.reverse_sibling(5); - }), - vec![MoveTo::ReverseChild(5)], - ), - ( - t(|t| { - t.down_to_temp(3); - t.up(); - }), - vec![], - ), - ( - t(|t| { - t.sibling(2); - t.up(); - t.down_to_temp(3); - }), - vec![MoveTo::Parent, MoveTo::TempChild(3)], - ), - ( - t(|t| { - t.up(); - t.down_to_temp(3); - }), - vec![MoveTo::Parent, MoveTo::TempChild(3)], - ), - ] { - let mut traversal = Traversal::new(); - traverse(&mut traversal); - let actual_moves: Vec<_> = traversal.commit().collect(); - assert_eq!(actual_moves, expected_moves); - } - } -} diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 491fdeabb..a36c3c208 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -64,8 +64,11 @@ pub struct VirtualDom { } #[derive(Clone, Copy)] -pub struct RealDomNode(u32); +pub struct RealDomNode(pub u32); impl RealDomNode { + pub fn new(id: u32) -> Self { + Self(id) + } pub fn empty() -> Self { Self(u32::MIN) } @@ -318,7 +321,8 @@ impl VirtualDom { cur_component.run_scope()?; // diff_machine.change_list.load_known_root(1); - let (old, new) = cur_component.get_frames_mut(); + let (old, new) = (cur_component.old_frame(), cur_component.next_frame()); + // let (old, new) = cur_component.get_frames_mut(); diff_machine.diff_node(old, new); // cur_height = cur_component.height; @@ -527,7 +531,8 @@ impl Scope { let EventTrigger { listener_id, event, .. } = trigger; - // + + // todo: implement scanning for outdated events unsafe { // Convert the raw ptr into an actual object // This operation is assumed to be safe @@ -547,12 +552,6 @@ impl Scope { Ok(()) } - fn get_frames_mut<'bump>( - &'bump mut self, - ) -> (&'bump mut VNode<'bump>, &'bump mut VNode<'bump>) { - todo!() - } - pub fn next_frame<'bump>(&'bump self) -> &'bump VNode<'bump> { self.frames.current_head_node() } diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 49154404d..3242c43ff 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -27,8 +27,8 @@ atoms = { path="../atoms" } # futures = "0.3.12" # html-validation = { path = "../html-validation", version = "0.1.1" } -async-channel = "1.6.1" -wee_alloc = "0.4.5" +# async-channel = "1.6.1" +nohash-hasher = "0.2.0" # futures-lite = "1.11.3" [dependencies.web-sys] @@ -79,6 +79,8 @@ opt-level = 's' crate-type = ["cdylib", "rlib"] [dev-dependencies] +im-rc = "15.0.0" +rand = { version="0.8.4", features=["small_rng"] } uuid = { version="0.8.2", features=["v4", "wasm-bindgen"] } [[example]] diff --git a/packages/web/examples/basic.rs b/packages/web/examples/basic.rs index fa36129fd..4cf3ec51d 100644 --- a/packages/web/examples/basic.rs +++ b/packages/web/examples/basic.rs @@ -1,5 +1,6 @@ //! Basic example that renders a simple VNode to the browser. +use dioxus_core as dioxus; use dioxus_core::prelude::*; use dioxus_web::*; @@ -50,15 +51,15 @@ static DocExamples: FC<()> = |ctx| { } }; - rsx! { - div {} - h1 {} - {""} - "asbasd" - dioxus::Fragment { - // - } - } + // rsx! { + // div {} + // h1 {} + // {""} + // "asbasd" + // dioxus::Fragment { + // // + // } + // } ctx.render(rsx! { div { diff --git a/packages/web/examples/context.rs b/packages/web/examples/context.rs index 17f2dea0e..888a652d8 100644 --- a/packages/web/examples/context.rs +++ b/packages/web/examples/context.rs @@ -2,14 +2,14 @@ //! -------------------- //! This example demonstrates how to use the raw context api for sharing state throughout the VirtualDOM Tree. //! A custom context must be its own unique type - otherwise use_context will fail. A context may be c -//! -//! -//! -//! -//! -//! -//! -//! +//! +//! +//! +//! +//! +//! +//! +//! use dioxus_core::prelude::*; use dioxus_web::WebsysRenderer; @@ -20,7 +20,6 @@ fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(Example)); } - #[derive(Debug)] struct CustomContext([&'static str; 3]); @@ -46,7 +45,6 @@ static Example: FC<()> = |ctx| { }) }; - #[derive(Props, PartialEq)] struct ButtonProps { id: u8, diff --git a/packages/web/examples/deep.rs b/packages/web/examples/deep.rs index 7779b816a..7143c1290 100644 --- a/packages/web/examples/deep.rs +++ b/packages/web/examples/deep.rs @@ -1,5 +1,6 @@ use std::rc::Rc; +use dioxus_core as dioxus; use dioxus_web::{dioxus::prelude::*, WebsysRenderer}; fn main() { diff --git a/packages/web/examples/demoday.rs b/packages/web/examples/demoday.rs index 68cbbe54a..50a295b9b 100644 --- a/packages/web/examples/demoday.rs +++ b/packages/web/examples/demoday.rs @@ -1,3 +1,4 @@ +use dioxus_core as dioxus; use dioxus_web::{dioxus::prelude::*, WebsysRenderer}; fn main() { diff --git a/packages/web/examples/derive.rs b/packages/web/examples/derive.rs index d249197d4..dc69e03be 100644 --- a/packages/web/examples/derive.rs +++ b/packages/web/examples/derive.rs @@ -8,10 +8,6 @@ fn main() { wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); } -// Use `wee_alloc` as the global allocator. -#[global_allocator] -static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; - fn App(ctx: Context<()>) -> VNode { let cansee = use_state_new(&ctx, || false); rsx! { in ctx, diff --git a/packages/web/examples/events.rs b/packages/web/examples/events.rs index 1049b1576..438fddcc6 100644 --- a/packages/web/examples/events.rs +++ b/packages/web/examples/events.rs @@ -5,17 +5,17 @@ use dioxus_core::prelude::*; fn main() {} fn autocomplete() { - let handler = move |evt| { - let r = evt.alt_key(); - if evt.alt_key() {} - }; + // let handler = move |evt| { + // let r = evt.alt_key(); + // if evt.alt_key() {} + // }; - let g = rsx! { - button { - button { - onclick: {handler} - } - } + // let g = rsx! { + // button { + // button { + // onclick: {handler} + // } + // } - }; + // }; } diff --git a/packages/web/examples/framework_benchmark.rs b/packages/web/examples/framework_benchmark.rs new file mode 100644 index 000000000..07d051ce4 --- /dev/null +++ b/packages/web/examples/framework_benchmark.rs @@ -0,0 +1,184 @@ +//! JS Framework Benchmark +//! ---------------------- +//! +//! This example is used in the JS framework benchmarking tool to compare Dioxus' performance with other frontend frameworks. +//! +//! +//! + +use std::rc::Rc; + +use dioxus::events::on::MouseEvent; +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_web::WebsysRenderer; + +fn main() { + wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); + console_error_panic_hook::set_once(); + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); +} + +// We use a special immutable hashmap to make hashmap operations efficient +type RowList = im_rc::HashMap, nohash_hasher::BuildNoHashHasher>; + +static App: FC<()> = |cx| { + let (items, set_items) = use_state(&cx, || RowList::default()); + let (selection, set_selection) = use_state(&cx, || None as Option); + + let create_rendered_rows = move |from, num| move |_| set_items(create_row_list(from, num)); + + let append_1_000_rows = + move |_| set_items(create_row_list(items.len(), 1000).union(items.clone())); + + let update_every_10th_row = move |_| { + let mut new_items = items.clone(); + let mut small_rng = SmallRng::from_entropy(); + new_items + .iter_mut() + .step_by(10) + .for_each(|(_, val)| *val = create_new_row_label(&mut small_rng)); + set_items(new_items); + }; + let clear_rows = move |_| set_items(RowList::default()); + + let swap_rows = move |_| { + // this looks a bit ugly because we're using a hashmap instead of a vec + if items.len() > 998 { + let mut new_items = items.clone(); + let a = new_items.get(&0).unwrap().clone(); + *new_items.get_mut(&0).unwrap() = new_items.get(&998).unwrap().clone(); + *new_items.get_mut(&998).unwrap() = a; + set_items(new_items); + } + }; + + let rows = items.iter().map(|(key, value)| { + rsx!(Row { + key: "{key}", + row_id: *key as usize, + label: value.clone(), + }) + }); + + cx.render(rsx! { + div { class: "container" + div { class: "jumbotron" + div { class: "row" + div { class: "col-md-6", h1 { "Dioxus" } } + div { class: "col-md-6" + div { class: "row" + ActionButton { name: "Create 1,000 rows", id: "run", action: create_rendered_rows(0, 1_000) } + ActionButton { name: "Create 10,000 rows", id: "runlots", action: create_rendered_rows(0, 10_000) } + ActionButton { name: "Append 1,000 rows", id: "add", action: append_1_000_rows } + ActionButton { name: "Update every 10th row", id: "update", action: update_every_10th_row, } + ActionButton { name: "Clear", id: "clear", action: clear_rows } + ActionButton { name: "Swap rows", id: "swaprows", action: swap_rows } + } + } + } + } + table { + tbody { + {rows} + } + } + span {} + } + }) +}; + +#[derive(Props)] +struct ActionButtonProps)> { + name: &'static str, + id: &'static str, + action: F, +} +fn ActionButton)>(cx: Context>) -> VNode { + cx.render(rsx! { + div { class: "col-sm-6 smallpad" + button {class:"btn btn-primary btn-block", type: "button", id: "{cx.id}", onclick: {&cx.action}, + "{cx.name}" + } + } + }) +} + + +#[derive(PartialEq, Props)] +struct RowProps { + row_id: usize, + label: Rc, +} +fn Row<'a>(cx: Context<'a, RowProps>) -> VNode { + cx.render(rsx! { + tr { + td { class:"col-md-1", "{cx.row_id}" } + td { class:"col-md-1", onclick: move |_| { /* run onselect */ } + a { class: "lbl", "{cx.label}" } + } + td { class: "col-md-1" + a { class: "remove", onclick: move |_| {/* remove */} + span { class: "glyphicon glyphicon-remove remove" aria_hidden: "true" } + } + } + td { class: "col-md-6" } + } + }) +} + +use rand::prelude::*; +fn create_new_row_label(rng: &mut SmallRng) -> Rc { + let mut label = String::new(); + label.push_str(ADJECTIVES.choose(rng).unwrap()); + label.push(' '); + label.push_str(COLOURS.choose(rng).unwrap()); + label.push(' '); + label.push_str(NOUNS.choose(rng).unwrap()); + Rc::from(label) +} + +fn create_row_list(from: usize, num: usize) -> RowList { + let mut small_rng = SmallRng::from_entropy(); + (from..num + from) + .map(|f| (f, create_new_row_label(&mut small_rng))) + .collect::() +} + +static ADJECTIVES: &[&str] = &[ + "pretty", + "large", + "big", + "small", + "tall", + "short", + "long", + "handsome", + "plain", + "quaint", + "clean", + "elegant", + "easy", + "angry", + "crazy", + "helpful", + "mushy", + "odd", + "unsightly", + "adorable", + "important", + "inexpensive", + "cheap", + "expensive", + "fancy", +]; + +static COLOURS: &[&str] = &[ + "red", "yellow", "blue", "green", "pink", "brown", "purple", "brown", "white", "black", + "orange", +]; + +static NOUNS: &[&str] = &[ + "table", "chair", "house", "bbq", "desk", "car", "pony", "cookie", "sandwich", "burger", + "pizza", "mouse", "keyboard", +]; diff --git a/packages/web/examples/hello.rs b/packages/web/examples/hello.rs index d595468f1..274379aa8 100644 --- a/packages/web/examples/hello.rs +++ b/packages/web/examples/hello.rs @@ -1,3 +1,4 @@ +use dioxus_core as dioxus; use dioxus_core::prelude::*; use dioxus_web::WebsysRenderer; @@ -10,12 +11,24 @@ fn main() { } static Example: FC<()> = |ctx| { + let nodes = (0..5).map(|f| { + rsx! { + li {"{f}"} + } + }); ctx.render(rsx! { div { span { class: "px-2 py-1 flex w-36 mt-4 items-center text-xs rounded-md font-semibold text-yellow-500 bg-yellow-100" - "DUE DATE : 18 JUN" + "DUE DATE : 189 JUN" } + p { + "these" + "are" + "text" + "nodes" + } + {nodes} } }) }; diff --git a/packages/web/examples/helloworld.rs b/packages/web/examples/helloworld.rs index 480adace4..ba7ca7da1 100644 --- a/packages/web/examples/helloworld.rs +++ b/packages/web/examples/helloworld.rs @@ -1,3 +1,4 @@ +use dioxus_core as dioxus; use dioxus_web::prelude::*; fn main() { diff --git a/packages/web/examples/infer.rs b/packages/web/examples/infer.rs index cab145280..101eb99f9 100644 --- a/packages/web/examples/infer.rs +++ b/packages/web/examples/infer.rs @@ -1,3 +1,4 @@ +use dioxus_core as dioxus; use dioxus_core::{events::on::MouseEvent, prelude::*}; use dioxus_web::WebsysRenderer; @@ -14,7 +15,7 @@ fn main() { static Example: FC<()> = |ctx| { let (event, set_event) = use_state(&ctx, || None); - let handler = move |evt: MouseEvent| { + let handler = move |evt| { set_event(Some(evt)); }; @@ -42,17 +43,15 @@ static Example: FC<()> = |ctx| { }) }; - #[derive(Debug, PartialEq, Props)] struct ExampleProps { - name: String + name: String, } static Example2: FC = |ctx| { - ctx.render(rsx!{ + ctx.render(rsx! { div { h1 {"hello {ctx.name}"} } }) }; - diff --git a/packages/web/examples/input.rs b/packages/web/examples/input.rs index 857c052ec..00239b9e3 100644 --- a/packages/web/examples/input.rs +++ b/packages/web/examples/input.rs @@ -26,25 +26,22 @@ static App: FC<()> = |ctx| { id: "username" type: "text" value: "{val}" - oninput: move |evet| { - log::debug!("Value is {:#?}", evet); - set_val(evet.value); - } + oninput: move |evet| set_val(evet.value()) } p { "Val is: {val}" } } } } - }) + }) }; static Example: FC<()> = |ctx| { ctx.render(rsx! { div { class: "max-w-lg max-w-xs bg-blue-800 shadow-2xl rounded-lg mx-auto text-center py-12 mt-4 rounded-xl" div { class: "container py-5 max-w-md mx-auto" - h1 { class: "text-gray-200 text-center font-extrabold -mt-3 text-3xl", + h1 { class: "text-gray-200 text-center font-extrabold -mt-3 text-3xl", "Text Input Example" - } + } UserInput {} } } @@ -54,19 +51,15 @@ static Example: FC<()> = |ctx| { static UserInput: FC<()> = |ctx| { let (val, set_val) = use_state(&ctx, || "asd".to_string()); - rsx!{ in ctx, + rsx! { in ctx, div { class: "mb-4" input { class: "shadow appearance-none rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" placeholder: "Username" - id: "username" + id: "username" type: "text" - oninput: move |evet| { - log::debug!("Value is {:#?}", evet); - set_val(evet.value); - } + oninput: move |evet| set_val(evet.value()) } p { "Val is: {val}" } } } - }; diff --git a/packages/web/examples/landingpage.rs b/packages/web/examples/landingpage.rs index f8bf56d2e..5401e72d0 100644 --- a/packages/web/examples/landingpage.rs +++ b/packages/web/examples/landingpage.rs @@ -6,7 +6,7 @@ use dioxus_core::prelude::*; use dioxus_web::*; fn main() { // Setup logging - wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); + // wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); console_error_panic_hook::set_once(); // Run the app wasm_bindgen_futures::spawn_local(WebsysRenderer::start(App)); diff --git a/packages/web/examples/list.rs b/packages/web/examples/list.rs index 933e151f8..1e62ffb30 100644 --- a/packages/web/examples/list.rs +++ b/packages/web/examples/list.rs @@ -53,7 +53,7 @@ static App: FC<()> = |ctx| { input { class: "new-todo" placeholder: "What needs to be done?" - oninput: move |evt| set_draft(evt.value) + oninput: move |evt| set_draft(evt.value()) } } diff --git a/packages/web/examples/many.rs b/packages/web/examples/many.rs index d595468f1..ea0ba5f4d 100644 --- a/packages/web/examples/many.rs +++ b/packages/web/examples/many.rs @@ -1,3 +1,4 @@ +use dioxus_core as dioxus; use dioxus_core::prelude::*; use dioxus_web::WebsysRenderer; diff --git a/packages/web/examples/props.rs b/packages/web/examples/props.rs new file mode 100644 index 000000000..09c4ecc08 --- /dev/null +++ b/packages/web/examples/props.rs @@ -0,0 +1,13 @@ +use dioxus_core as dioxus; +use dioxus_core::prelude::*; + +#[derive(Props)] +struct MyProps<'a> { + blah: u128, + b: &'a (), +} + +fn main() { + // let p = unsafe { MyProps {}.memoize(&MyProps {}) }; + // dbg!(p); +} diff --git a/packages/web/examples/rsxt.rs b/packages/web/examples/rsxt.rs index 94fb9407d..5f6abf667 100644 --- a/packages/web/examples/rsxt.rs +++ b/packages/web/examples/rsxt.rs @@ -1,14 +1,18 @@ #![allow(non_snake_case)] -use dioxus_core as dioxus; +use std::rc::Rc; + use dioxus::{events::on::MouseEvent, prelude::*}; +use dioxus_core as dioxus; use dioxus_web::WebsysRenderer; fn main() { wasm_logger::init(wasm_logger::Config::new(log::Level::Trace)); console_error_panic_hook::set_once(); - + wasm_bindgen_futures::spawn_local(async { - let props = ExampleProps { initial_name: "..?"}; + let props = ExampleProps { + initial_name: "..?", + }; WebsysRenderer::new_with_props(Example, props) .run() .await @@ -25,17 +29,17 @@ static Example: FC = |ctx| { let name = use_state_new(&ctx, move || ctx.initial_name); ctx.render(rsx! { - div { + div { class: "py-12 px-4 text-center w-full max-w-2xl mx-auto" - span { + span { class: "text-sm font-semibold" "Dioxus Example: Jack and Jill" } - h2 { - class: "text-5xl mt-2 mb-6 leading-tight font-semibold font-heading" + h2 { + class: "text-5xl mt-2 mb-6 leading-tight font-semibold font-heading" "Hello, {name}" } - + CustomButton { name: "Jack!", handler: move |_| name.set("Jack") } CustomButton { name: "Jill!", handler: move |_| name.set("Jill") } CustomButton { name: "Bob!", handler: move |_| name.set("Bob")} @@ -45,12 +49,10 @@ static Example: FC = |ctx| { }) }; - - #[derive(Props)] -struct ButtonProps<'src, F: Fn(MouseEvent)> { +struct ButtonProps<'src, F: Fn(Rc)> { name: &'src str, - handler: F + handler: F, } fn CustomButton<'a, F: Fn(MouseEvent)>(ctx: Context<'a, ButtonProps<'a, F>>) -> VNode { @@ -69,13 +71,12 @@ impl PartialEq for ButtonProps<'_, F> { } } - #[derive(Props, PartialEq)] struct PlaceholderProps { - val: &'static str + val: &'static str, } fn Placeholder(ctx: Context) -> VNode { - ctx.render(rsx!{ + ctx.render(rsx! { div { "child: {ctx.val}" } diff --git a/packages/web/examples/todomvc/main.rs b/packages/web/examples/todomvc/main.rs index 3518c0bff..f199ff226 100644 --- a/packages/web/examples/todomvc/main.rs +++ b/packages/web/examples/todomvc/main.rs @@ -1,10 +1,11 @@ +use dioxus_core as dioxus; use dioxus_web::{prelude::*, WebsysRenderer}; -mod filtertoggles; -mod recoil; -mod state; -mod todoitem; -mod todolist; +// mod filtertoggles; +// mod recoil; +// mod state; +// mod todoitem; +// mod todolist; static APP_STYLE: &'static str = include_str!("./style.css"); @@ -13,10 +14,10 @@ fn main() { ctx.render(rsx! { div { id: "app" - style { "{APP_STYLE}" } + // style { "{APP_STYLE}" } // list - todolist::TodoList {} + // todolist::TodoList {} // footer footer { diff --git a/packages/web/examples/todomvc_simple.rs b/packages/web/examples/todomvc_simple.rs index d2cf43242..82440ae13 100644 --- a/packages/web/examples/todomvc_simple.rs +++ b/packages/web/examples/todomvc_simple.rs @@ -1,5 +1,6 @@ use std::{collections::HashMap, rc::Rc}; +use dioxus_core as dioxus; use dioxus_core::prelude::*; use dioxus_web::WebsysRenderer; @@ -65,7 +66,7 @@ pub fn TodoList(ctx: Context<()>) -> VNode { class: "new-todo" placeholder: "What needs to be done?" value: "{draft}" - oninput: move |evt| set_draft(evt.value) + oninput: move |evt| set_draft(evt.value()) } } @@ -78,7 +79,7 @@ pub fn TodoList(ctx: Context<()>) -> VNode { FilterState::Completed => item.checked, }) .map(|(id, item)| { - TodoEntry!(); + // TodoEntry!(); todo!() // rsx!(TodoEntry { // key: "{order}", @@ -100,17 +101,14 @@ pub struct TodoEntryProps { item: Rc, } -pub fn TodoEntry(ctx: Context, props: &TodoEntryProps) -> VNode { -// #[inline_props] -pub fn TodoEntry( - ctx: Context, - baller: &impl Fn() -> (), - caller: &impl Fn() -> (), - todo: &Rc, -) -> VNode { - // pub fn TodoEntry(ctx: Context, todo: &Rc) -> VNode { +pub fn TodoEntry(ctx: Context) -> VNode { let (is_editing, set_is_editing) = use_state(&ctx, || false); - // let todo = &ctx.item; + let contents = ""; + let todo = TodoItem { + checked: false, + contents: "asd".to_string(), + id: uuid::Uuid::new_v4(), + }; ctx.render(rsx! ( li { diff --git a/packages/web/examples/todomvcsingle.rs b/packages/web/examples/todomvcsingle.rs index 8320cd198..771539fc9 100644 --- a/packages/web/examples/todomvcsingle.rs +++ b/packages/web/examples/todomvcsingle.rs @@ -9,7 +9,7 @@ //! Here, we show to use Dioxus' Recoil state management solution to simplify app logic #![allow(non_snake_case)] use dioxus_web::dioxus::prelude::*; -use recoil::*; + use std::collections::HashMap; use uuid::Uuid; diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 9f1a583f1..420199f67 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -63,62 +63,70 @@ impl WebsysRenderer { } pub async fn run(&mut self) -> dioxus_core::error::Result<()> { - let (sender, mut receiver) = async_channel::unbounded::(); + // let (sender, mut receiver) = async_channel::unbounded::(); let body_element = prepare_websys_dom(); - let mut patch_machine = interpreter::PatchMachine::new(body_element.clone(), move |ev| { - log::debug!("Event trigger! {:#?}", ev); - let mut c = sender.clone(); - wasm_bindgen_futures::spawn_local(async move { - c.send(ev).await.unwrap(); - }); - }); + // let mut patch_machine = interpreter::PatchMachine::new(body_element.clone(), move |ev| { + // log::debug!("Event trigger! {:#?}", ev); + // let mut c = sender.clone(); + // wasm_bindgen_futures::spawn_local(async move { + // c.send(ev).await.unwrap(); + // }); + // }); + let root_node = body_element.first_child().unwrap(); - patch_machine.stack.push(root_node.clone()); + + let mut websys_dom = crate::new::WebsysDom::new(body_element); + + websys_dom.stack.push(root_node); + // patch_machine.stack.push(root_node.clone()); // todo: initialize the event registry properly on the root - let edits = self.internal_dom.rebuild()?; - log::debug!("Received edits: {:#?}", edits); - edits.iter().for_each(|edit| { - log::debug!("patching with {:?}", edit); - patch_machine.handle_edit(edit); - }); + self.internal_dom.rebuild(&mut websys_dom)?; + // let edits = self.internal_dom.rebuild()?; + // log::debug!("Received edits: {:#?}", edits); + // edits.iter().for_each(|edit| { + // log::debug!("patching with {:?}", edit); + // patch_machine.handle_edit(edit); + // }); - patch_machine.reset(); - let root_node = body_element.first_child().unwrap(); - patch_machine.stack.push(root_node.clone()); + // patch_machine.reset(); + // let root_node = body_element.first_child().unwrap(); + // patch_machine.stack.push(root_node.clone()); // log::debug!("patch stack size {:?}", patch_machine.stack); // Event loop waits for the receiver to finish up // TODO! Connect the sender to the virtual dom's suspense system // Suspense is basically an external event that can force renders to specific nodes - while let Ok(event) = receiver.recv().await { - log::debug!("Stack before entrance {:#?}", patch_machine.stack.top()); - // log::debug!("patch stack size before {:#?}", patch_machine.stack); - // patch_machine.reset(); - // patch_machine.stack.push(root_node.clone()); - let edits = self.internal_dom.progress_with_event(event)?; - log::debug!("Received edits: {:#?}", edits); + // while let Ok(event) = receiver.recv().await { + // log::debug!("Stack before entrance {:#?}", patch_machine.stack.top()); + // log::debug!("patch stack size before {:#?}", patch_machine.stack); + // patch_machine.reset(); + // patch_machine.stack.push(root_node.clone()); + // self.internal_dom + // .progress_with_event(&mut websys_dom, event)?; + // let edits = self.internal_dom.progress_with_event(event)?; + // log::debug!("Received edits: {:#?}", edits); - for edit in &edits { - // log::debug!("edit stream {:?}", edit); - // log::debug!("Stream stack {:#?}", patch_machine.stack.top()); - patch_machine.handle_edit(edit); - } + // for edit in &edits { + // // log::debug!("edit stream {:?}", edit); + // // log::debug!("Stream stack {:#?}", patch_machine.stack.top()); + // patch_machine.handle_edit(edit); + // } - // log::debug!("patch stack size after {:#?}", patch_machine.stack); - patch_machine.reset(); - // our root node reference gets invalidated - // not sure why - // for now, just select the first child again. - // eventually, we'll just make our own root element instead of using body - // or just use body directly IDEK - let root_node = body_element.first_child().unwrap(); - patch_machine.stack.push(root_node.clone()); - } + // log::debug!("patch stack size after {:#?}", patch_machine.stack); + // patch_machine.reset(); + // our root node reference gets invalidated + // not sure why + // for now, just select the first child again. + // eventually, we'll just make our own root element instead of using body + // or just use body directly IDEK + // let root_node = body_element.first_child().unwrap(); + // patch_machine.stack.push(root_node.clone()); + // } Ok(()) // should actually never return from this, should be an error, rustc just cant see it } diff --git a/packages/web/src/new.rs b/packages/web/src/new.rs index 1c94305f3..f43cdc42e 100644 --- a/packages/web/src/new.rs +++ b/packages/web/src/new.rs @@ -1,2 +1,275 @@ -pub struct WebsysDom {} -impl WebsysDom {} +use std::collections::HashMap; + +use dioxus_core::virtual_dom::RealDomNode; +use nohash_hasher::IntMap; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::{ + window, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, Node, +}; + +use crate::interpreter::Stack; +pub struct WebsysDom { + pub stack: Stack, + nodes: IntMap, + document: Document, + root: Element, + + // We need to make sure to add comments between text nodes + // We ensure that the text siblings are patched by preventing the browser from merging + // neighboring text nodes. Originally inspired by some of React's work from 2016. + // -> https://reactjs.org/blog/2016/04/07/react-v15.html#major-changes + // -> https://github.com/facebook/react/pull/5753 + // + // `ptns` = Percy text node separator + // TODO + last_node_was_text: bool, + + node_counter: Counter, +} +impl WebsysDom { + pub fn new(root: Element) -> Self { + let document = window() + .expect("must have access to the window") + .document() + .expect("must have access to the Document"); + + Self { + stack: Stack::with_capacity(10), + nodes: HashMap::with_capacity_and_hasher( + 1000, + nohash_hasher::BuildNoHashHasher::default(), + ), + document, + root, + last_node_was_text: false, + node_counter: Counter(0), + } + } +} + +struct Counter(u32); +impl Counter { + fn next(&mut self) -> u32 { + self.0 += 1; + self.0 + } +} +impl dioxus_core::diff::RealDom for WebsysDom { + fn push_root(&mut self, root: dioxus_core::virtual_dom::RealDomNode) { + let domnode = self.nodes.get(&root.0).expect("Failed to pop know root"); + self.stack.push(domnode.clone()); + } + + fn append_child(&mut self) { + let child = self.stack.pop(); + + if child.dyn_ref::().is_some() { + if self.last_node_was_text { + let comment_node = self + .document + .create_comment("dioxus") + .dyn_into::() + .unwrap(); + self.stack.top().append_child(&comment_node).unwrap(); + } + self.last_node_was_text = true; + } else { + self.last_node_was_text = false; + } + + self.stack.top().append_child(&child).unwrap(); + } + + fn replace_with(&mut self) { + let new_node = self.stack.pop(); + let old_node = self.stack.pop(); + + if old_node.has_type::() { + old_node + .dyn_ref::() + .unwrap() + .replace_with_with_node_1(&new_node) + .unwrap(); + } else if old_node.has_type::() { + old_node + .dyn_ref::() + .unwrap() + .replace_with_with_node_1(&new_node) + .unwrap(); + } else if old_node.has_type::() { + old_node + .dyn_ref::() + .unwrap() + .replace_with_with_node_1(&new_node) + .unwrap(); + } else { + panic!("Cannot replace node: {:?}", old_node); + } + + // // poc to see if this is a valid solution + // if let Some(id) = self.current_known { + // // update mapping + // self.known_roots.insert(id, new_node.clone()); + // self.current_known = None; + // } + + self.stack.push(new_node); + } + + fn remove(&mut self) { + todo!() + } + + fn remove_all_children(&mut self) { + todo!() + } + + fn create_text_node(&mut self, text: &str) -> dioxus_core::virtual_dom::RealDomNode { + let nid = self.node_counter.next(); + let textnode = self + .document + .create_text_node(text) + .dyn_into::() + .unwrap(); + self.stack.push(textnode.clone()); + self.nodes.insert(nid, textnode); + + RealDomNode::new(nid) + } + + fn create_element(&mut self, tag: &str) -> dioxus_core::virtual_dom::RealDomNode { + let el = self + .document + .create_element(tag) + .unwrap() + .dyn_into::() + .unwrap(); + + self.stack.push(el.clone()); + let nid = self.node_counter.next(); + self.nodes.insert(nid, el); + RealDomNode::new(nid) + } + + fn create_element_ns( + &mut self, + tag: &str, + namespace: &str, + ) -> dioxus_core::virtual_dom::RealDomNode { + let el = self + .document + .create_element_ns(Some(namespace), tag) + .unwrap() + .dyn_into::() + .unwrap(); + + self.stack.push(el.clone()); + let nid = self.node_counter.next(); + self.nodes.insert(nid, el); + RealDomNode::new(nid) + } + + fn new_event_listener( + &mut self, + event: &str, + scope: dioxus_core::prelude::ScopeIdx, + id: usize, + ) { + // if let Some(entry) = self.listeners.get_mut(event) { + // entry.0 += 1; + // } else { + // let trigger = self.trigger.clone(); + // let handler = Closure::wrap(Box::new(move |event: &web_sys::Event| { + // log::debug!("Handling event!"); + + // let target = event + // .target() + // .expect("missing target") + // .dyn_into::() + // .expect("not a valid element"); + + // let typ = event.type_(); + + // let gi_id: Option = target + // .get_attribute(&format!("dioxus-giid-{}", typ)) + // .and_then(|v| v.parse().ok()); + + // let gi_gen: Option = target + // .get_attribute(&format!("dioxus-gigen-{}", typ)) + // .and_then(|v| v.parse().ok()); + + // let li_idx: Option = target + // .get_attribute(&format!("dioxus-lidx-{}", typ)) + // .and_then(|v| v.parse().ok()); + + // if let (Some(gi_id), Some(gi_gen), Some(li_idx)) = (gi_id, gi_gen, li_idx) { + // // Call the trigger + // log::debug!( + // "decoded gi_id: {}, gi_gen: {}, li_idx: {}", + // gi_id, + // gi_gen, + // li_idx + // ); + + // let triggered_scope = ScopeIdx::from_raw_parts(gi_id, gi_gen); + // trigger.0.as_ref()(EventTrigger::new( + // virtual_event_from_websys_event(event), + // triggered_scope, + // // scope, + // li_idx, + // )); + // } + // }) as Box); + + // self.root + // .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) + // .unwrap(); + + // // Increment the listeners + // self.listeners.insert(event.into(), (1, handler)); + // } + } + + fn remove_event_listener(&mut self, event: &str) { + todo!() + } + + fn set_text(&mut self, text: &str) { + self.stack.top().set_text_content(Some(text)) + } + + fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool) { + if name == "class" { + if let Some(el) = self.stack.top().dyn_ref::() { + el.set_class_name(value); + } + } else { + } + } + + fn remove_attribute(&mut self, name: &str) { + let node = self.stack.top(); + if let Some(node) = node.dyn_ref::() { + node.remove_attribute(name).unwrap(); + } + if let Some(node) = node.dyn_ref::() { + // Some attributes are "volatile" and don't work through `removeAttribute`. + if name == "value" { + node.set_value(""); + } + if name == "checked" { + node.set_checked(false); + } + } + + if let Some(node) = node.dyn_ref::() { + if name == "selected" { + node.set_selected(true); + } + } + } + + fn raw_node_as_any_mut(&self) -> &mut dyn std::any::Any { + todo!() + } +} From 9d7ee79826a3b3fb952a70abcbb16dcd3363d2fb Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Jun 2021 01:44:48 -0400 Subject: [PATCH 07/20] feat: events work again! --- Cargo.toml | 2 +- notes/TODO.md | 4 +- packages/core/.vscode/settings.json | 2 +- packages/core/Cargo.toml | 7 +- packages/core/src/debug_renderer.rs | 7 +- packages/core/src/diff.rs | 112 +++--- packages/core/src/events.rs | 38 +- packages/core/src/nodebuilder.rs | 61 +-- packages/core/src/nodes.rs | 26 +- packages/core/src/virtual_dom.rs | 75 ++-- packages/web/Cargo.toml | 3 +- packages/web/examples/demoday.rs | 4 +- packages/web/examples/hello.rs | 6 +- packages/web/examples/jackjill.rs | 3 +- packages/web/examples/listy.rs | 28 ++ packages/web/src/lib.rs | 34 +- packages/web/src/new.rs | 430 +++++++++++++++++++--- packages/web/src/{ => old}/interpreter.rs | 1 - 18 files changed, 621 insertions(+), 222 deletions(-) create mode 100644 packages/web/examples/listy.rs rename packages/web/src/{ => old}/interpreter.rs (99%) diff --git a/Cargo.toml b/Cargo.toml index 6dbd0ac8b..64f7992b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ members = [ "packages/core", "packages/html-namespace", "packages/web", - "packages/cli", + # "packages/cli", # "packages/atoms", # "packages/ssr", # "packages/docsite", diff --git a/notes/TODO.md b/notes/TODO.md index d1fff211a..749636698 100644 --- a/notes/TODO.md +++ b/notes/TODO.md @@ -2,7 +2,7 @@ - [] Transition away from names and towards compile-time safe tags - [] Fix diffing of fragments - [] Properly integrate memoization to prevent safety issues with children -- [x] Understand the issue with callbacks (outdated generations) +- [] Understand and fix the issue with callbacks (outdated generations) - [] Fix examples for core, web, ssr, and general - [] Finish up documentation - [] Polish the Recoil (Dirac?) API @@ -16,3 +16,5 @@ - [] ...some how deserialize (hydrate) the dom state? - [] Implement controlled inputs for select and textarea - [] ...somehow... noderefs.... + +use_state(&cx, ) diff --git a/packages/core/.vscode/settings.json b/packages/core/.vscode/settings.json index 80f51cff8..2f418701b 100644 --- a/packages/core/.vscode/settings.json +++ b/packages/core/.vscode/settings.json @@ -1,3 +1,3 @@ { - "rust-analyzer.inlayHints.enable": false + "rust-analyzer.inlayHints.enable": true } \ No newline at end of file diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index c77791ede..4fec2cafe 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -16,7 +16,7 @@ dioxus-core-macro = { path="../core-macro", version="0.1.1" } # Backs scopes and graphs between parent and children generational-arena = { version="0.2.8" } -# Bumpalo backs the VNode creation +# Bumpalo is used as a micro heap backing each component bumpalo = { version="3.6.0", features=["collections", "boxed"] } # custom error type @@ -25,6 +25,9 @@ thiserror = "1" # faster hashmaps fxhash = "0.2.1" +# even *faster* hashmaps for index-based types +nohash-hasher = "0.2.0" + # Used in diffing longest-increasing-subsequence = "0.1.0" @@ -33,8 +36,10 @@ log = "0.4" # Serialize the Edits for use in Webview/Liveview instances serde = { version="1", features=["derive"], optional=true } + smallvec = "1.6.1" + [features] default = [] serialize = ["serde", "generational-arena/serde"] diff --git a/packages/core/src/debug_renderer.rs b/packages/core/src/debug_renderer.rs index 605758bc9..28e906727 100644 --- a/packages/core/src/debug_renderer.rs +++ b/packages/core/src/debug_renderer.rs @@ -16,7 +16,7 @@ impl DebugRenderer { /// /// This means that the root component must either consumes its own context, or statics are used to generate the page. /// The root component can access things like routing in its context. - pub fn new(root: impl for<'a> Fn(Context<'a, ()>) -> VNode + 'static) -> Self { + pub fn new(root: FC<()>) -> Self { Self::new_with_props(root, ()) } @@ -24,10 +24,7 @@ impl DebugRenderer { /// Automatically progresses the creation of the VNode tree to completion. /// /// A VDom is automatically created. If you want more granular control of the VDom, use `from_vdom` - pub fn new_with_props( - root: impl for<'a> Fn(Context<'a, T>) -> VNode + 'static, - root_props: T, - ) -> Self { + pub fn new_with_props(root: FC, root_props: T) -> Self { Self::from_vdom(VirtualDom::new_with_props(root, root_props)) } diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index f235c4535..9cbd2fe65 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -60,7 +60,13 @@ pub trait RealDom { fn create_element_ns(&mut self, tag: &str, namespace: &str) -> RealDomNode; // events - fn new_event_listener(&mut self, event: &str, scope: ScopeIdx, id: usize); + fn new_event_listener( + &mut self, + event: &str, + scope: ScopeIdx, + element_id: usize, + realnode: RealDomNode, + ); // fn new_event_listener(&mut self, event: &str); fn remove_event_listener(&mut self, event: &str); @@ -138,6 +144,7 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { self.dom.replace_with(); return; } + new.dom_id.set(old.dom_id.get()); self.diff_listeners(old.listeners, new.listeners); self.diff_attr(old.attributes, new.attributes, new.namespace.is_some()); @@ -145,6 +152,7 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { } // New node is a text element, need to replace the element with a simple text node VNode::Text(_) => { + log::debug!("Replacing el with text"); self.create(new_node); self.dom.replace_with(); } @@ -169,8 +177,10 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { VNode::Text(old) => match new_node { VNode::Text(new) => { if old.text != new.text { + log::debug!("Text has changed {}, {}", old.text, new.text); self.dom.set_text(new.text); } + new.dom_id.set(old.dom_id.get()); } VNode::Element(_) | VNode::Component(_) => { self.create(new_node); @@ -316,10 +326,10 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { }; el.dom_id.set(real_id); - listeners.iter().enumerate().for_each(|(_id, listener)| { - todo!() - // dom - // .new_event_listener(listener.event, listener.scope, listener.id) + listeners.iter().enumerate().for_each(|(idx, listener)| { + self.dom + .new_event_listener(listener.event, listener.scope, idx, real_id); + listener.mounted_node.set(real_id); }); for attr in *attributes { @@ -333,12 +343,12 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // to emit three instructions to (1) create a text node, (2) set its // text content, and finally (3) append the text node to this // parent. - if children.len() == 1 { - if let VNode::Text(text) = &children[0] { - self.dom.set_text(text.text); - return; - } - } + // if children.len() == 1 { + // if let VNode::Text(text) = &children[0] { + // self.dom.set_text(text.text); + // return; + // } + // } for child in *children { self.create(child); @@ -405,8 +415,8 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { new_component.run_scope().unwrap(); // And then run the diff algorithm - todo!(); - // self.diff_node(new_component.old_frame(), new_component.next_frame()); + // todo!(); + self.diff_node(new_component.old_frame(), new_component.next_frame()); // Finally, insert this node as a seen node. self.seen_nodes.insert(idx); @@ -473,6 +483,8 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { if !old.is_empty() || !new.is_empty() { // self.dom.commit_traversal(); } + // TODO + // what does "diffing listeners" even mean? 'outer1: for (_l_idx, new_l) in new.iter().enumerate() { // go through each new listener @@ -480,34 +492,34 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // if any characteristics changed, remove and then re-add // if nothing changed, then just move on - let event_type = new_l.event; for old_l in old { if new_l.event == old_l.event { - if new_l.id != old_l.id { - self.dom.remove_event_listener(event_type); - // TODO! we need to mess with events and assign them by RealDomNode - // self.dom - // .update_event_listener(event_type, new_l.scope, new_l.id) - } + new_l.mounted_node.set(old_l.mounted_node.get()); + // if new_l.id != old_l.id { + // self.dom.remove_event_listener(event_type); + // // TODO! we need to mess with events and assign them by RealDomNode + // // self.dom + // // .update_event_listener(event_type, new_l.scope, new_l.id) + // } continue 'outer1; } } - self.dom - .new_event_listener(event_type, new_l.scope, new_l.id); + // self.dom + // .new_event_listener(event_type, new_l.scope, new_l.id); } - 'outer2: for old_l in old { - for new_l in new { - if new_l.event == old_l.event { - continue 'outer2; - } - } - self.dom.remove_event_listener(old_l.event); - } + // 'outer2: for old_l in old { + // for new_l in new { + // if new_l.event == old_l.event { + // continue 'outer2; + // } + // } + // self.dom.remove_event_listener(old_l.event); + // } } // Diff a node's attributes. @@ -590,11 +602,12 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // Don't take this fast path... } - (_, VNode::Text(text)) => { - // self.dom.commit_traversal(); - self.dom.set_text(text.text); - return; - } + // (_, VNode::Text(text)) => { + // // self.dom.commit_traversal(); + // log::debug!("using optimized text set"); + // self.dom.set_text(text.text); + // return; + // } // todo: any more optimizations (_, _) => {} @@ -1047,7 +1060,7 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // [... parent] // // the change list stack is in the same state when this function returns. - fn diff_non_keyed_children(&self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { + fn diff_non_keyed_children(&mut self, old: &'a [VNode<'a>], new: &'a [VNode<'a>]) { // Handled these cases in `diff_children` before calling this function. debug_assert!(!new.is_empty()); debug_assert!(!old.is_empty()); @@ -1057,13 +1070,26 @@ impl<'a, Dom: RealDom> DiffMachine<'a, Dom> { // self.dom.push_root() // [... parent child] - todo!() - // for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() { - // // [... parent prev_child] - // self.dom.go_to_sibling(i); - // // [... parent this_child] - // self.diff_node(old_child, new_child); - // } + // todo!() + for (i, (new_child, old_child)) in new.iter().zip(old.iter()).enumerate() { + // [... parent prev_child] + // self.dom.go_to_sibling(i); + // [... parent this_child] + self.dom.push_root(old_child.get_mounted_id().unwrap()); + self.diff_node(old_child, new_child); + + let old_id = old_child.get_mounted_id().unwrap(); + let new_id = new_child.get_mounted_id().unwrap(); + + log::debug!( + "pushed root. {:?}, {:?}", + old_child.get_mounted_id().unwrap(), + new_child.get_mounted_id().unwrap() + ); + if old_id != new_id { + log::debug!("Mismatch: {:?}", new_child); + } + } // match old.len().cmp(&new.len()) { // // old.len > new.len -> removing some nodes diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index d5b6a4d47..1f400c41b 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -6,20 +6,20 @@ use std::rc::Rc; -use crate::innerlude::ScopeIdx; +use crate::{innerlude::ScopeIdx, virtual_dom::RealDomNode}; #[derive(Debug)] pub struct EventTrigger { pub component_id: ScopeIdx, - pub listener_id: usize, + pub real_node_id: RealDomNode, pub event: VirtualEvent, } impl EventTrigger { - pub fn new(event: VirtualEvent, scope: ScopeIdx, id: usize) -> Self { + pub fn new(event: VirtualEvent, scope: ScopeIdx, mounted_dom_id: RealDomNode) -> Self { Self { component_id: scope, - listener_id: id, + real_node_id: mounted_dom_id, event, } } @@ -55,17 +55,15 @@ pub mod on { use crate::{ builder::ElementBuilder, builder::NodeCtx, - innerlude::{Attribute, Listener, VNode}, + innerlude::{Attribute, Listener, RealDomNode, VNode}, }; + use std::cell::Cell; use super::VirtualEvent; macro_rules! event_directory { ( $( $eventdata:ident: [ $( $name:ident )* ]; )* ) => { $( - - - $( pub fn $name<'a>( c: &'_ NodeCtx<'a>, @@ -74,7 +72,7 @@ pub mod on { let bump = &c.bump(); Listener { event: stringify!($name), - id: *c.listener_id.borrow(), + mounted_node: bump.alloc(Cell::new(RealDomNode::empty())), scope: c.scope_ref.arena_idx, callback: bump.alloc(move |evt: VirtualEvent| match evt { VirtualEvent::$eventdata(event) => callback(event), @@ -121,31 +119,31 @@ pub mod on { /// Returns whether or not a specific event is a bubbling event fn bubbles(&self) -> bool; /// Sets or returns whether the event should propagate up the hierarchy or not - fn cancelBubble(&self) -> (); + fn cancel_bubble(&self); /// Returns whether or not an event can have its default action prevented fn cancelable(&self) -> bool; /// Returns whether the event is composed or not fn composed(&self) -> bool; /// Returns the event's path - fn composedPath(&self) -> (); + fn composed_path(&self) -> String; /// Returns the element whose event listeners triggered the event - fn currentTarget(&self) -> (); + fn current_target(&self); /// Returns whether or not the preventDefault method was called for the event - fn defaultPrevented(&self) -> (); + fn default_prevented(&self) -> bool; /// Returns which phase of the event flow is currently being evaluated - fn eventPhase(&self) -> (); + fn event_phase(&self) -> usize; /// Returns whether or not an event is trusted - fn isTrusted(&self) -> (); + fn is_trusted(&self) -> bool; /// Cancels the event if it is cancelable, meaning that the default action that belongs to the event will - fn preventDefault(&self) -> (); + fn prevent_default(&self); /// Prevents other listeners of the same event from being called - fn stopImmediatePropagation(&self) -> (); + fn stop_immediate_propagation(&self); /// Prevents further propagation of an event during event flow - fn stopPropagation(&self) -> (); + fn stop_propagation(&self); /// Returns the element that triggered the event - fn target(&self) -> (); + fn target(&self); /// Returns the time (in milliseconds relative to the epoch) at which the event was created - fn timeStamp(&self) -> usize; + fn time_stamp(&self) -> usize; } pub trait ClipboardEvent: Debug { diff --git a/packages/core/src/nodebuilder.rs b/packages/core/src/nodebuilder.rs index ff8f04c4c..c3ea6e93a 100644 --- a/packages/core/src/nodebuilder.rs +++ b/packages/core/src/nodebuilder.rs @@ -1,7 +1,12 @@ //! Helpers for building virtual DOM VNodes. use std::{ - any::Any, borrow::BorrowMut, cell::RefCell, fmt::Arguments, intrinsics::transmute, u128, + any::Any, + borrow::BorrowMut, + cell::{Cell, RefCell}, + fmt::Arguments, + intrinsics::transmute, + u128, }; use crate::{ @@ -9,7 +14,7 @@ use crate::{ innerlude::{Properties, VComponent, FC}, nodes::{Attribute, Listener, NodeKey, VNode}, prelude::{VElement, VFragment}, - virtual_dom::Scope, + virtual_dom::{RealDomNode, Scope}, }; /// A virtual DOM element builder. @@ -353,12 +358,11 @@ where /// ``` pub fn on(self, event: &'static str, callback: impl Fn(VirtualEvent) + 'a) -> Self { let bump = &self.ctx.bump(); - let listener = Listener { event, callback: bump.alloc(callback), - id: *self.ctx.listener_id.borrow(), scope: self.ctx.scope_ref.arena_idx, + mounted_node: bump.alloc(Cell::new(RealDomNode::empty())), }; self.add_listener(listener) } @@ -367,7 +371,8 @@ where self.listeners.push(listener); // bump the context id forward - *self.ctx.listener_id.borrow_mut() += 1; + let id = self.ctx.listener_id.get(); + self.ctx.listener_id.set(id + 1); // Add this listener to the context list // This casts the listener to a self-referential pointer @@ -378,7 +383,7 @@ where .scope_ref .listeners .borrow_mut() - .push(r.callback as *const _); + .push((r.mounted_node as *const _, r.callback as *const _)); }); self @@ -499,15 +504,19 @@ where let child = item.into_vnode(&self.ctx); self.children.push(child); } - let len_after = self.children.len(); - if len_after > len_before { - let last_child = self.children.last().unwrap(); - if last_child.key().is_none() { - // TODO: Somehow get the name of the component when NodeCtx is being made - const ERR_MSG: &str = r#"Warning: Each child in an array or iterator should have a unique "key" prop. - Check the render method of XXXX. - See fb.me/react-warning-keys for more information. "#; - log::error!("{}", ERR_MSG); + if self.children.len() > len_before + 1 { + if self.children.last().unwrap().key().is_none() { + if cfg!(debug_assertions) { + log::error!( + r#" +Warning: Each child in an array or iterator should have a unique "key" prop. +Not providing a key will lead to poor performance with lists. +See docs.rs/dioxus for more information. +--- +To help you identify where this error is coming from, we've generated a backtrace. + "#, + ); + } } } self @@ -549,17 +558,6 @@ impl<'a, F> VNodeBuilder<'a, LazyNodes<'a, F>> for LazyNodes<'a, F> where { } -// Cover the cases where nodes are pre-rendered. -// Likely used by enums. -// ---- -// let nodes = ctx.render(rsx!{ ... }; -// rsx! { {nodes } } -// impl<'a> IntoVNode<'a> for VNode { -// fn into_vnode(self, _ctx: &NodeCtx<'a>) -> VNode<'a> { -// self.root -// } -// } - // Wrap the the node-builder closure in a concrete type. // --- // This is a bit of a hack to implement the IntoVNode trait for closure types. @@ -611,12 +609,14 @@ where impl<'a> IntoVNode<'a> for () { fn into_vnode(self, ctx: &NodeCtx<'a>) -> VNode<'a> { + todo!(); VNode::Suspended } } impl<'a> IntoVNode<'a> for Option<()> { fn into_vnode(self, ctx: &NodeCtx<'a>) -> VNode<'a> { + todo!(); VNode::Suspended } } @@ -735,7 +735,8 @@ impl<'a, 'b> ChildrenList<'a, 'b> { #[derive(Clone)] pub struct NodeCtx<'a> { pub scope_ref: &'a Scope, - pub listener_id: RefCell, + pub listener_id: Cell, + // pub listener_id: RefCell, } impl<'a> NodeCtx<'a> { @@ -744,11 +745,13 @@ impl<'a> NodeCtx<'a> { &self.scope_ref.cur_frame().bump } - fn text(&self, args: Arguments) -> VNode<'a> { + /// Create some text that's allocated along with the other vnodes + pub fn text(&self, args: Arguments) -> VNode<'a> { text3(self.bump(), args) } - fn element<'b, Listeners, Attributes, Children>( + /// Create an element builder + pub fn element<'b, Listeners, Attributes, Children>( &'b self, tag_name: &'static str, ) -> ElementBuilder< diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index cf245785d..ba98d8e07 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -13,7 +13,7 @@ use bumpalo::Bump; use std::{ any::Any, cell::{Cell, RefCell}, - fmt::{Arguments, Debug}, + fmt::{Arguments, Debug, Formatter}, marker::PhantomData, rc::Rc, }; @@ -127,8 +127,8 @@ impl<'a> VNode<'a> { pub fn get_mounted_id(&self) -> Option { match self { - VNode::Element(_) => todo!(), - VNode::Text(_) => todo!(), + VNode::Element(el) => Some(el.dom_id.get()), + VNode::Text(te) => Some(te.dom_id.get()), VNode::Fragment(_) => todo!(), VNode::Suspended => todo!(), VNode::Component(_) => todo!(), @@ -136,6 +136,18 @@ impl<'a> VNode<'a> { } } +impl Debug for VNode<'_> { + fn fmt(&self, s: &mut Formatter<'_>) -> std::result::Result<(), std::fmt::Error> { + match self { + VNode::Element(el) => write!(s, "element, {}", el.tag_name), + VNode::Text(t) => write!(s, "text, {}", t.text), + VNode::Fragment(_) => write!(s, "fragment"), + VNode::Suspended => write!(s, "suspended"), + VNode::Component(_) => write!(s, "component"), + } + } +} + #[derive(Clone)] pub struct VText<'src> { pub text: &'src str, @@ -201,11 +213,14 @@ pub struct Listener<'bump> { /// The type of event to listen for. pub(crate) event: &'static str, + /// Which scope? + /// This might not actually be relevant pub scope: ScopeIdx, - pub id: usize, + + pub mounted_node: &'bump Cell, /// The callback to invoke when the event happens. - pub(crate) callback: &'bump (dyn Fn(VirtualEvent)), + pub(crate) callback: &'bump dyn Fn(VirtualEvent), } /// The key for keyed children. @@ -259,6 +274,7 @@ pub struct VComponent<'src> { pub key: NodeKey<'src>, pub mounted_root: Cell, + pub ass_scope: RefCell, // pub comparator: Rc bool + 'src>, diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index a36c3c208..55f80c4f9 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -24,7 +24,7 @@ use bumpalo::Bump; use generational_arena::Arena; use std::{ any::{Any, TypeId}, - cell::RefCell, + cell::{Cell, RefCell}, collections::{HashMap, HashSet, VecDeque}, fmt::Debug, future::Future, @@ -63,7 +63,7 @@ pub struct VirtualDom { _root_prop_type: std::any::TypeId, } -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq)] pub struct RealDomNode(pub u32); impl RealDomNode { pub fn new(id: u32) -> Self { @@ -109,7 +109,7 @@ impl VirtualDom { /// /// let dom = VirtualDom::new(Example); /// ``` - pub fn new(root: impl Fn(Context<()>) -> VNode + 'static) -> Self { + pub fn new(root: FC<()>) -> Self { Self::new_with_props(root, ()) } @@ -141,10 +141,7 @@ impl VirtualDom { /// /// let dom = VirtualDom::new(Example); /// ``` - pub fn new_with_props( - root: impl Fn(Context

) -> VNode + 'static, - root_props: P, - ) -> Self { + pub fn new_with_props(root: FC

, root_props: P) -> Self { let components = ScopeArena::new(Arena::new()); // Normally, a component would be passed as a child in the RSX macro which automatically produces OpaqueComponents @@ -260,11 +257,11 @@ impl VirtualDom { pub fn progress_with_event( &mut self, realdom: &mut Dom, - event: EventTrigger, + trigger: EventTrigger, ) -> Result<()> { - let id = event.component_id.clone(); + let id = trigger.component_id.clone(); - self.components.try_get_mut(id)?.call_listener(event)?; + self.components.try_get_mut(id)?.call_listener(trigger)?; let mut diff_machine = DiffMachine::new( realdom, @@ -397,7 +394,12 @@ pub struct Scope { // - is self-refenrential and therefore needs to point into the bump // Stores references into the listeners attached to the vnodes // NEEDS TO BE PRIVATE - pub(crate) listeners: RefCell>, + pub(crate) listeners: RefCell, *const dyn Fn(VirtualEvent))>>, + // pub(crate) listeners: RefCell>, + // pub(crate) listeners: RefCell>, + // pub(crate) listeners: RefCell>, + // NoHashMap + // pub(crate) listeners: RefCell>, } // We need to pin the hook so it doesn't move as we initialize the list of hooks @@ -450,7 +452,7 @@ impl Scope { let child_nodes = unsafe { std::mem::transmute(child_nodes) }; Self { - child_nodes: child_nodes, + child_nodes, caller, parent, arena_idx, @@ -490,12 +492,12 @@ impl Scope { self.frames.next().bump.reset(); // Remove all the outdated listeners - // - self.listeners - .try_borrow_mut() - .ok() - .ok_or(Error::FatalInternal("Borrowing listener failed"))? - .drain(..); + self.listeners.borrow_mut().clear(); + // self.listeners + // .try_borrow_mut() + // .ok() + // .ok_or(Error::FatalInternal("Borrowing listener failed"))? + // .drain(..); *self.hookidx.borrow_mut() = 0; @@ -529,26 +531,35 @@ impl Scope { // The listener list will be completely drained because the next frame will write over previous listeners pub fn call_listener(&mut self, trigger: EventTrigger) -> Result<()> { let EventTrigger { - listener_id, event, .. + real_node_id, + event, + .. } = trigger; // todo: implement scanning for outdated events - unsafe { - // Convert the raw ptr into an actual object - // This operation is assumed to be safe - let listener_fn = self - .listeners - .try_borrow() - .ok() - .ok_or(Error::FatalInternal("Borrowing listener failed"))? - .get(listener_id as usize) - .ok_or(Error::FatalInternal("Event should exist if triggered"))? - .as_ref() - .ok_or(Error::FatalInternal("Raw event ptr is invalid"))?; - // Run the callback with the user event + // Convert the raw ptr into an actual object + // This operation is assumed to be safe + + log::debug!("Calling listeners! {:?}", self.listeners.borrow().len()); + let listners = self.listeners.borrow(); + let (_, listener) = listners + .iter() + .find(|(domptr, _)| { + let p = unsafe { &**domptr }; + p.get() == real_node_id + }) + .expect(&format!( + "Failed to find real node with ID {:?}", + real_node_id + )); + + // TODO: Don'tdo a linear scan! Do a hashmap lookup! It'll be faster! + unsafe { + let listener_fn = &**listener; listener_fn(event); } + Ok(()) } diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 3242c43ff..c00682739 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -27,8 +27,9 @@ atoms = { path="../atoms" } # futures = "0.3.12" # html-validation = { path = "../html-validation", version = "0.1.1" } -# async-channel = "1.6.1" +async-channel = "1.6.1" nohash-hasher = "0.2.0" +anyhow = "1.0.41" # futures-lite = "1.11.3" [dependencies.web-sys] diff --git a/packages/web/examples/demoday.rs b/packages/web/examples/demoday.rs index 50a295b9b..7a9a34385 100644 --- a/packages/web/examples/demoday.rs +++ b/packages/web/examples/demoday.rs @@ -11,7 +11,7 @@ fn App(ctx: Context<()>) -> VNode { ctx.render(rsx! { main { class: "dark:bg-gray-800 bg-white relative h-screen" NavBar {} - {(0..10).map(|f| rsx!{ Landing {} })} + {(0..10).map(|f| rsx!(Landing { key: "{f}" }))} } }) } @@ -62,7 +62,7 @@ fn Landing(ctx: Context<()>) -> VNode { div { class: "container mx-auto px-6 flex flex-col justify-between items-center relative py-8" div { class: "flex flex-col" h1 { class: "font-light w-full uppercase text-center text-4xl sm:text-5xl dark:text-white text-gray-800" - "The React Framework for Production" + "The Dioxus Framework for Production" } h2{ class: "font-light max-w-2xl mx-auto w-full text-xl dark:text-white text-gray-500 text-center py-8" "Next.js gives you the best developer experience with all the features you need for production: \n diff --git a/packages/web/examples/hello.rs b/packages/web/examples/hello.rs index 274379aa8..d6a2c198e 100644 --- a/packages/web/examples/hello.rs +++ b/packages/web/examples/hello.rs @@ -11,11 +11,7 @@ fn main() { } static Example: FC<()> = |ctx| { - let nodes = (0..5).map(|f| { - rsx! { - li {"{f}"} - } - }); + let nodes = (0..15).map(|f| rsx! (li { key: "{f}", "{f}"})); ctx.render(rsx! { div { span { diff --git a/packages/web/examples/jackjill.rs b/packages/web/examples/jackjill.rs index da5958bb9..da58dc59a 100644 --- a/packages/web/examples/jackjill.rs +++ b/packages/web/examples/jackjill.rs @@ -33,12 +33,11 @@ static Example: FC<()> = |ctx| { diff --git a/packages/web/examples/listy.rs b/packages/web/examples/listy.rs new file mode 100644 index 000000000..82e6b3f55 --- /dev/null +++ b/packages/web/examples/listy.rs @@ -0,0 +1,28 @@ +use dioxus_core as dioxus; +use dioxus_core::prelude::*; +use dioxus_web::WebsysRenderer; + +fn main() { + wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); + console_error_panic_hook::set_once(); + + log::info!("hello world"); + wasm_bindgen_futures::spawn_local(WebsysRenderer::start(JonsFavoriteCustomApp)); +} + +fn JonsFavoriteCustomApp(cx: Context<()>) -> VNode { + let items = (0..20).map(|f| { + rsx! { + li {"{f}"} + } + }); + + cx.render(rsx! { + div { + "list" + ul { + {items} + } + } + }) +} diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index 420199f67..411e8b9a7 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -12,7 +12,7 @@ pub use dioxus_core as dioxus; use dioxus_core::{events::EventTrigger, prelude::FC}; pub use dioxus_core::prelude; -pub mod interpreter; +// pub mod interpreter; pub mod new; /// The `WebsysRenderer` provides a way of rendering a Dioxus Virtual DOM to the browser's DOM. @@ -34,7 +34,7 @@ impl WebsysRenderer { /// /// Run the app to completion, panicing if any error occurs while rendering. /// Pairs well with the wasm_bindgen async handler - pub async fn start(root: impl for<'a> Fn(Context<'a, ()>) -> VNode + 'static) { + pub async fn start(root: FC<()>) { Self::new(root).run().await.expect("Virtual DOM failed :("); } @@ -42,7 +42,7 @@ impl WebsysRenderer { /// /// This means that the root component must either consumes its own context, or statics are used to generate the page. /// The root component can access things like routing in its context. - pub fn new(root: impl for<'a> Fn(Context<'a, ()>) -> VNode + 'static) -> Self { + pub fn new(root: FC<()>) -> Self { Self::new_with_props(root, ()) } @@ -50,10 +50,7 @@ impl WebsysRenderer { /// Automatically progresses the creation of the VNode tree to completion. /// /// A VDom is automatically created. If you want more granular control of the VDom, use `from_vdom` - pub fn new_with_props( - root: impl for<'a> Fn(Context<'a, T>) -> VNode + 'static, - root_props: T, - ) -> Self { + pub fn new_with_props(root: FC, root_props: T) -> Self { Self::from_vdom(VirtualDom::new_with_props(root, root_props)) } @@ -63,28 +60,23 @@ impl WebsysRenderer { } pub async fn run(&mut self) -> dioxus_core::error::Result<()> { - // let (sender, mut receiver) = async_channel::unbounded::(); - let body_element = prepare_websys_dom(); - // let mut patch_machine = interpreter::PatchMachine::new(body_element.clone(), move |ev| { - // log::debug!("Event trigger! {:#?}", ev); - // let mut c = sender.clone(); - // wasm_bindgen_futures::spawn_local(async move { - // c.send(ev).await.unwrap(); - // }); - // }); - let root_node = body_element.first_child().unwrap(); - let mut websys_dom = crate::new::WebsysDom::new(body_element); + let mut websys_dom = crate::new::WebsysDom::new(body_element.clone()); websys_dom.stack.push(root_node); - // patch_machine.stack.push(root_node.clone()); - - // todo: initialize the event registry properly on the root self.internal_dom.rebuild(&mut websys_dom)?; + + while let Some(trigger) = websys_dom.wait_for_event().await { + let root_node = body_element.first_child().unwrap(); + websys_dom.stack.push(root_node.clone()); + self.internal_dom + .progress_with_event(&mut websys_dom, trigger)?; + } + // let edits = self.internal_dom.rebuild()?; // log::debug!("Received edits: {:#?}", edits); // edits.iter().for_each(|edit| { diff --git a/packages/web/src/new.rs b/packages/web/src/new.rs index f43cdc42e..5c79dea25 100644 --- a/packages/web/src/new.rs +++ b/packages/web/src/new.rs @@ -1,19 +1,35 @@ -use std::collections::HashMap; +use std::{collections::HashMap, rc::Rc, sync::Arc}; -use dioxus_core::virtual_dom::RealDomNode; +use dioxus_core::{ + events::{EventTrigger, VirtualEvent}, + prelude::ScopeIdx, + virtual_dom::RealDomNode, +}; +use fxhash::FxHashMap; use nohash_hasher::IntMap; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{ window, Document, Element, Event, HtmlElement, HtmlInputElement, HtmlOptionElement, Node, }; -use crate::interpreter::Stack; pub struct WebsysDom { pub stack: Stack, nodes: IntMap, document: Document, root: Element, + event_receiver: async_channel::Receiver, + trigger: Arc, + + // every callback gets a monotomically increasing callback ID + callback_id: usize, + + // map of listener types to number of those listeners + listeners: FxHashMap)>, + + // Map of callback_id to component index and listener id + callback_map: FxHashMap, + // We need to make sure to add comments between text nodes // We ensure that the text siblings are patched by preventing the browser from merging // neighboring text nodes. Originally inspired by some of React's work from 2016. @@ -33,18 +49,39 @@ impl WebsysDom { .document() .expect("must have access to the Document"); + let (sender, mut receiver) = async_channel::unbounded::(); + + let sender_callback = Arc::new(move |ev| { + let mut c = sender.clone(); + wasm_bindgen_futures::spawn_local(async move { + c.send(ev).await.unwrap(); + }); + }); + + let mut nodes = + HashMap::with_capacity_and_hasher(1000, nohash_hasher::BuildNoHashHasher::default()); + + nodes.insert(0_u32, root.clone().dyn_into::().unwrap()); Self { stack: Stack::with_capacity(10), - nodes: HashMap::with_capacity_and_hasher( - 1000, - nohash_hasher::BuildNoHashHasher::default(), - ), + nodes, + + callback_id: 0, + listeners: FxHashMap::default(), + callback_map: FxHashMap::default(), document, + event_receiver: receiver, + trigger: sender_callback, root, last_node_was_text: false, node_counter: Counter(0), } } + + pub async fn wait_for_event(&mut self) -> Option { + let v = self.event_receiver.recv().await.unwrap(); + Some(v) + } } struct Counter(u32); @@ -56,11 +93,13 @@ impl Counter { } impl dioxus_core::diff::RealDom for WebsysDom { fn push_root(&mut self, root: dioxus_core::virtual_dom::RealDomNode) { + log::debug!("Called `[`push_root] {:?}", root); let domnode = self.nodes.get(&root.0).expect("Failed to pop know root"); self.stack.push(domnode.clone()); } fn append_child(&mut self) { + log::debug!("Called [`append_child`]"); let child = self.stack.pop(); if child.dyn_ref::().is_some() { @@ -81,6 +120,7 @@ impl dioxus_core::diff::RealDom for WebsysDom { } fn replace_with(&mut self) { + log::debug!("Called [`replace_with`]"); let new_node = self.stack.pop(); let old_node = self.stack.pop(); @@ -117,10 +157,12 @@ impl dioxus_core::diff::RealDom for WebsysDom { } fn remove(&mut self) { + log::debug!("Called [`remove`]"); todo!() } fn remove_all_children(&mut self) { + log::debug!("Called [`remove_all_children`]"); todo!() } @@ -134,6 +176,8 @@ impl dioxus_core::diff::RealDom for WebsysDom { self.stack.push(textnode.clone()); self.nodes.insert(nid, textnode); + log::debug!("Called [`create_text_node`]: {}, {}", text, nid); + RealDomNode::new(nid) } @@ -148,6 +192,7 @@ impl dioxus_core::diff::RealDom for WebsysDom { self.stack.push(el.clone()); let nid = self.node_counter.next(); self.nodes.insert(nid, el); + log::debug!("Called [`create_element`]: {}, {:?}", tag, nid); RealDomNode::new(nid) } @@ -166,6 +211,7 @@ impl dioxus_core::diff::RealDom for WebsysDom { self.stack.push(el.clone()); let nid = self.node_counter.next(); self.nodes.insert(nid, el); + log::debug!("Called [`create_element_ns`]: {:}", nid); RealDomNode::new(nid) } @@ -173,81 +219,133 @@ impl dioxus_core::diff::RealDom for WebsysDom { &mut self, event: &str, scope: dioxus_core::prelude::ScopeIdx, - id: usize, + el_id: usize, + real_id: RealDomNode, ) { - // if let Some(entry) = self.listeners.get_mut(event) { - // entry.0 += 1; - // } else { - // let trigger = self.trigger.clone(); - // let handler = Closure::wrap(Box::new(move |event: &web_sys::Event| { - // log::debug!("Handling event!"); + log::debug!( + "Called [`new_event_listener`]: {}, {:?}, {}, {:?}", + event, + scope, + el_id, + real_id + ); + // attach the correct attributes to the element + // these will be used by accessing the event's target + // This ensures we only ever have one handler attached to the root, but decide + // dynamically when we want to call a listener. - // let target = event - // .target() - // .expect("missing target") - // .dyn_into::() - // .expect("not a valid element"); + let el = self.stack.top(); - // let typ = event.type_(); + let el = el + .dyn_ref::() + .expect(&format!("not an element: {:?}", el)); - // let gi_id: Option = target - // .get_attribute(&format!("dioxus-giid-{}", typ)) - // .and_then(|v| v.parse().ok()); + let (gi_id, gi_gen) = (&scope).into_raw_parts(); + el.set_attribute( + &format!("dioxus-event-{}", event), + &format!("{}.{}.{}.{}", gi_id, gi_gen, el_id, real_id.0), + ) + .unwrap(); - // let gi_gen: Option = target - // .get_attribute(&format!("dioxus-gigen-{}", typ)) - // .and_then(|v| v.parse().ok()); + // Register the callback to decode - // let li_idx: Option = target - // .get_attribute(&format!("dioxus-lidx-{}", typ)) - // .and_then(|v| v.parse().ok()); + if let Some(entry) = self.listeners.get_mut(event) { + entry.0 += 1; + } else { + let trigger = self.trigger.clone(); + let handler = Closure::wrap(Box::new(move |event: &web_sys::Event| { + // "Result" cannot be received from JS + // Instead, we just build and immediately execute a closure that returns result + let res = || -> anyhow::Result { + log::debug!("Handling event!"); - // if let (Some(gi_id), Some(gi_gen), Some(li_idx)) = (gi_id, gi_gen, li_idx) { - // // Call the trigger - // log::debug!( - // "decoded gi_id: {}, gi_gen: {}, li_idx: {}", - // gi_id, - // gi_gen, - // li_idx - // ); + let target = event + .target() + .expect("missing target") + .dyn_into::() + .expect("not a valid element"); - // let triggered_scope = ScopeIdx::from_raw_parts(gi_id, gi_gen); - // trigger.0.as_ref()(EventTrigger::new( - // virtual_event_from_websys_event(event), - // triggered_scope, - // // scope, - // li_idx, - // )); - // } - // }) as Box); + let typ = event.type_(); + use anyhow::Context; + let val: String = target + .get_attribute(&format!("dioxus-event-{}", typ)) + .context("")?; - // self.root - // .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) - // .unwrap(); + let mut fields = val.splitn(4, "."); - // // Increment the listeners - // self.listeners.insert(event.into(), (1, handler)); - // } + let gi_id = fields + .next() + .and_then(|f| f.parse::().ok()) + .context("")?; + let gi_gen = fields + .next() + .and_then(|f| f.parse::().ok()) + .context("")?; + let el_id = fields + .next() + .and_then(|f| f.parse::().ok()) + .context("")?; + let real_id = fields + .next() + .and_then(|f| f.parse::().ok().map(RealDomNode::new)) + .context("")?; + + // Call the trigger + log::debug!( + "decoded gi_id: {}, gi_gen: {}, li_idx: {}", + gi_id, + gi_gen, + el_id + ); + + let triggered_scope = ScopeIdx::from_raw_parts(gi_id, gi_gen); + Ok(EventTrigger::new( + virtual_event_from_websys_event(event), + triggered_scope, + real_id, + )) + }; + + match res() { + Ok(synthetic_event) => trigger.as_ref()(synthetic_event), + Err(_) => log::error!("Error decoding Dioxus event attribute."), + }; + }) as Box); + + self.root + .add_event_listener_with_callback(event, (&handler).as_ref().unchecked_ref()) + .unwrap(); + + // Increment the listeners + self.listeners.insert(event.into(), (1, handler)); + } } fn remove_event_listener(&mut self, event: &str) { + log::debug!("Called [`remove_event_listener`]: {}", event); todo!() } fn set_text(&mut self, text: &str) { + log::debug!("Called [`set_text`]: {}", text); self.stack.top().set_text_content(Some(text)) } fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool) { + log::debug!("Called [`set_attribute`]: {}, {}", name, value); if name == "class" { if let Some(el) = self.stack.top().dyn_ref::() { el.set_class_name(value); } } else { + if let Some(el) = self.stack.top().dyn_ref::() { + el.set_attribute(name, value).unwrap(); + } } } fn remove_attribute(&mut self, name: &str) { + log::debug!("Called [`remove_attribute`]: {}", name); let node = self.stack.top(); if let Some(node) = node.dyn_ref::() { node.remove_attribute(name).unwrap(); @@ -270,6 +368,234 @@ impl dioxus_core::diff::RealDom for WebsysDom { } fn raw_node_as_any_mut(&self) -> &mut dyn std::any::Any { + log::debug!("Called [`raw_node_as_any_mut`]"); todo!() } } + +#[derive(Debug, Default)] +pub struct Stack { + list: Vec, +} + +impl Stack { + pub fn with_capacity(cap: usize) -> Self { + Stack { + list: Vec::with_capacity(cap), + } + } + + pub fn push(&mut self, node: Node) { + // debug!("stack-push: {:?}", node); + self.list.push(node); + } + + pub fn pop(&mut self) -> Node { + let res = self.list.pop().unwrap(); + res + } + + pub fn clear(&mut self) { + self.list.clear(); + } + + pub fn top(&self) -> &Node { + match self.list.last() { + Some(a) => a, + None => panic!("Called 'top' of an empty stack, make sure to push the root first"), + } + } +} + +fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { + use dioxus_core::events::on::*; + match event.type_().as_str() { + "copy" | "cut" | "paste" => { + // let evt: web_sys::ClipboardEvent = event.clone().dyn_into().unwrap(); + + todo!() + } + + "compositionend" | "compositionstart" | "compositionupdate" => { + let evt: web_sys::CompositionEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "keydown" | "keypress" | "keyup" => { + let evt: web_sys::KeyboardEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "focus" | "blur" => { + let evt: web_sys::FocusEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "change" => { + let evt: web_sys::Event = event.clone().dyn_into().expect("wrong error typ"); + todo!() + // VirtualEvent::FormEvent(FormEvent {value:}) + } + + "input" | "invalid" | "reset" | "submit" => { + // is a special react events + let evt: web_sys::InputEvent = event.clone().dyn_into().expect("wrong event type"); + let this: web_sys::EventTarget = evt.target().unwrap(); + + let value = (&this) + .dyn_ref() + .map(|input: &web_sys::HtmlInputElement| input.value()) + .or_else(|| { + (&this) + .dyn_ref() + .map(|input: &web_sys::HtmlTextAreaElement| input.value()) + }) + .or_else(|| { + (&this) + .dyn_ref::() + .unwrap() + .text_content() + }) + .expect("only an InputElement or TextAreaElement or an element with contenteditable=true can have an oninput event listener"); + + // let p2 = evt.data_transfer(); + + // let value: Option = (&evt).data(); + // let value = val; + // let value = value.unwrap_or_default(); + // let value = (&evt).data().expect("No data to unwrap"); + + // todo - this needs to be a "controlled" event + // these events won't carry the right data with them + todo!() + // VirtualEvent::FormEvent(FormEvent { value }) + } + + "click" | "contextmenu" | "doubleclick" | "drag" | "dragend" | "dragenter" | "dragexit" + | "dragleave" | "dragover" | "dragstart" | "drop" | "mousedown" | "mouseenter" + | "mouseleave" | "mousemove" | "mouseout" | "mouseover" | "mouseup" => { + let evt: web_sys::MouseEvent = event.clone().dyn_into().unwrap(); + + #[derive(Debug)] + pub struct CustomMouseEvent(web_sys::MouseEvent); + impl dioxus_core::events::on::MouseEvent for CustomMouseEvent { + fn alt_key(&self) -> bool { + todo!() + } + fn button(&self) -> i32 { + todo!() + } + fn buttons(&self) -> i32 { + todo!() + } + fn client_x(&self) -> i32 { + todo!() + } + fn client_y(&self) -> i32 { + todo!() + } + fn ctrl_key(&self) -> bool { + todo!() + } + fn meta_key(&self) -> bool { + todo!() + } + fn page_x(&self) -> i32 { + todo!() + } + fn page_y(&self) -> i32 { + todo!() + } + fn screen_x(&self) -> i32 { + todo!() + } + fn screen_y(&self) -> i32 { + todo!() + } + fn shift_key(&self) -> bool { + todo!() + } + fn get_modifier_state(&self, key_code: usize) -> bool { + todo!() + } + } + VirtualEvent::MouseEvent(Rc::new(CustomMouseEvent(evt))) + // MouseEvent(Box::new(RawMouseEvent { + // alt_key: evt.alt_key(), + // button: evt.button() as i32, + // buttons: evt.buttons() as i32, + // client_x: evt.client_x(), + // client_y: evt.client_y(), + // ctrl_key: evt.ctrl_key(), + // meta_key: evt.meta_key(), + // page_x: evt.page_x(), + // page_y: evt.page_y(), + // screen_x: evt.screen_x(), + // screen_y: evt.screen_y(), + // shift_key: evt.shift_key(), + // get_modifier_state: GetModifierKey(Box::new(|f| { + // // evt.get_modifier_state(f) + // todo!("This is not yet implemented properly, sorry :("); + // })), + // })) + // todo!() + // VirtualEvent::MouseEvent() + } + + "pointerdown" | "pointermove" | "pointerup" | "pointercancel" | "gotpointercapture" + | "lostpointercapture" | "pointerenter" | "pointerleave" | "pointerover" | "pointerout" => { + let evt: web_sys::PointerEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "select" => { + // let evt: web_sys::SelectionEvent = event.clone().dyn_into().unwrap(); + // not required to construct anything special beyond standard event stuff + todo!() + } + + "touchcancel" | "touchend" | "touchmove" | "touchstart" => { + let evt: web_sys::TouchEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "scroll" => { + // let evt: web_sys::UIEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "wheel" => { + let evt: web_sys::WheelEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "abort" | "canplay" | "canplaythrough" | "durationchange" | "emptied" | "encrypted" + | "ended" | "error" | "loadeddata" | "loadedmetadata" | "loadstart" | "pause" | "play" + | "playing" | "progress" | "ratechange" | "seeked" | "seeking" | "stalled" | "suspend" + | "timeupdate" | "volumechange" | "waiting" => { + // not required to construct anything special beyond standard event stuff + + // let evt: web_sys::MediaEvent = event.clone().dyn_into().unwrap(); + // let evt: web_sys::MediaEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "animationstart" | "animationend" | "animationiteration" => { + let evt: web_sys::AnimationEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "transitionend" => { + let evt: web_sys::TransitionEvent = event.clone().dyn_into().unwrap(); + todo!() + } + + "toggle" => { + // not required to construct anything special beyond standard event stuff (target) + + // let evt: web_sys::ToggleEvent = event.clone().dyn_into().unwrap(); + todo!() + } + _ => VirtualEvent::OtherEvent, + } +} diff --git a/packages/web/src/interpreter.rs b/packages/web/src/old/interpreter.rs similarity index 99% rename from packages/web/src/interpreter.rs rename to packages/web/src/old/interpreter.rs index f494f3766..c6410922e 100644 --- a/packages/web/src/interpreter.rs +++ b/packages/web/src/old/interpreter.rs @@ -2,7 +2,6 @@ use std::{borrow::Borrow, convert::TryInto, default, fmt::Debug, sync::Arc}; use dioxus_core::{ events::{EventTrigger, VirtualEvent}, - patch::Edit, prelude::ScopeIdx, }; use fxhash::FxHashMap; From f5683a23464992ecace463a61414795b5a2c58c8 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Jun 2021 22:32:54 -0400 Subject: [PATCH 08/20] wip: docs Worked a bit on adding more examples. Trying out a new "antipattern" example to show how *not* to use Dioxus. --- examples/antipatterns.rs | 62 ++++++++++ examples/app.rs | 143 ---------------------- examples/reducer.rs | 56 +++++++++ packages/core-macro/examples/prop_test.rs | 9 +- packages/core/src/events.rs | 16 ++- packages/core/src/nodebuilder.rs | 6 +- packages/html-namespace/examples/poc.rs | 30 +++++ packages/web/src/new.rs | 35 +++--- src/lib.rs | 14 ++- 9 files changed, 197 insertions(+), 174 deletions(-) create mode 100644 examples/antipatterns.rs delete mode 100644 examples/app.rs create mode 100644 examples/reducer.rs create mode 100644 packages/html-namespace/examples/poc.rs diff --git a/examples/antipatterns.rs b/examples/antipatterns.rs new file mode 100644 index 000000000..69562bbde --- /dev/null +++ b/examples/antipatterns.rs @@ -0,0 +1,62 @@ +//! Example: Antipatterns +//! --------------------- +//! +//! This example shows what *not* to do and provides a reason why a given pattern is considered an "AntiPattern". Most +//! anti-patterns are considered wrong to due performance reasons or violate the "rules" of Dioxus. These rules are +//! borrowed from other successful UI frameworks, and Dioxus is more focused on providing a familiar, ergonomic interface +//! rather than building new harder-to-misuse patterns. +use std::collections::HashMap; + +use dioxus::prelude::*; +fn main() {} + +/// Antipattern: Iterators without keys +/// ----------------------------------- +/// +/// This is considered an anti-pattern for performance reasons. Dioxus must diff your current and old layout and must +/// take a slower path if it can't correlate old elements with new elements. Lists are particularly susceptible to the +/// "slow" path, so you're strongly encouraged to provide a unique ID stable between renders. +/// +/// Dioxus will log an error in the console if it detects that your iterator does not properly generate keys +#[derive(PartialEq, Props)] +struct NoKeysProps { + data: HashMap, +} +static AntipatternNoKeys: FC = |cx| { + // WRONG: Make sure to add keys! + rsx!(in cx, ul { + {cx.data.iter().map(|(k, v)| rsx!(li { "List item: {v}" }))} + }); + // Like this: + rsx!(in cx, ul { + {cx.data.iter().map(|(k, v)| rsx!(li { key: "{k}", "List item: {v}" }))} + }) +}; + +/// Antipattern: Deeply nested fragments +/// ------------------------------------ +/// +/// This particular antipattern is not necessarily an antipattern in other frameworks but does has a performance impact +/// in Dioxus apps. Fragments don't mount a physical element to the dom immediately, so Dioxus must recurse into its +/// children to find a physical dom node. This process is called "normalization". Other frameworks perform an agressive +/// mutative normalization while Dioxus keeps your VNodes immutable. This means that deepely nested fragments make Dioxus +/// perform unnecessary work. Prefer one or two levels of fragments / nested components until presenting a true dom element. +/// +/// Only Component and Fragment nodes are susceptible to this issue. Dioxus mitigates this with components by providing +/// an API for registering shared state without the ContextProvider pattern. +static Blah: FC<()> = |cx| { + // Try to avoid + rsx!(in cx, + Fragment { + Fragment { + Fragment { + Fragment { + Fragment { + div { "Finally have a real node!" } + } + } + } + } + } + ) +}; diff --git a/examples/app.rs b/examples/app.rs deleted file mode 100644 index 7c4bb63f6..000000000 --- a/examples/app.rs +++ /dev/null @@ -1,143 +0,0 @@ -#![allow(unused)] -/* -This example shows how to encapsulate sate in dioxus components with the reducer pattern. -This pattern is very useful when a single component can handle many types of input that can -be represented by an enum. This particular pattern is very powerful in rust where ADTs can simplify -much of the traditional reducer boilerplate. -*/ - -fn main() {} -use dioxus::prelude::*; -use std::future::Future; - -enum Actions { - Pause, - Play, -} - -struct SomeState { - is_playing: bool, -} - -impl SomeState { - fn new() -> Self { - Self { is_playing: false } - } - fn reduce(&mut self, action: Actions) { - match action { - Actions::Pause => self.is_playing = false, - Actions::Play => self.is_playing = true, - } - } - fn is_playing(&self) -> &'static str { - match self.is_playing { - true => "currently playing!", - false => "not currently playing", - } - } -} - -pub static ExampleReducer: FC<()> = |ctx| { - let (state, reduce) = use_reducer(&ctx, SomeState::new, SomeState::reduce); - - let is_playing = state.is_playing(); - - ctx.render(rsx! { - div { - h1 {"Select an option"} - h3 {"The radio is... {is_playing}!"} - button { - "Pause" - onclick: move |_| reduce(Actions::Pause) - } - button { - "Play" - onclick: move |_| reduce(Actions::Play) - } - } - }) -}; - -/* - - - - - - - - - - - - - - -*/ - -struct AppContext { - name: String, -} - -enum KindaState { - Ready, - Complete, - Erred, -} - -static EnumReducer: FC<()> = |ctx| { - let (state, reduce) = use_reducer(&ctx, || KindaState::Ready, |cur, new| *cur = new); - - let status = match state { - KindaState::Ready => "Ready", - KindaState::Complete => "Complete", - KindaState::Erred => "Erred", - }; - - ctx.render(rsx! { - div { - h1 {"{status}"} - button { - "Set Ready" - onclick: move |_| reduce(KindaState::Ready) - } - button { - "Set Complete" - onclick: move |_| reduce(KindaState::Complete) - } - button { - "Set Erred" - onclick: move |_| reduce(KindaState::Erred) - } - ul { - {(0..10).map(|f| { - - rsx!{ - li { - "hello there!" - } - } - })} - } - } - }) -}; - -/// Demonstrate how the DebugRenderer can be used to unit test components without needing a browser -/// These tests can run locally. -/// They use the "compare" method of the debug renderer to do partial tree compares for succint -#[test] -fn ensure_it_works_properly() -> dioxus::error::Result<()> { - let mut test = DebugRenderer::new(EnumReducer); - test.compare(rsx! { div { h1 {"Ready"} } })?; - - test.trigger_listener(1)?; - test.compare(rsx! { div { h1 {"Ready"} } })?; - - test.trigger_listener(2)?; - test.compare(rsx! { div { h1 {"Complete"} } })?; - - test.trigger_listener(3)?; - test.compare(rsx! { div { h1 {"Erred"} } })?; - Ok(()) -} diff --git a/examples/reducer.rs b/examples/reducer.rs new file mode 100644 index 000000000..beafdfe19 --- /dev/null +++ b/examples/reducer.rs @@ -0,0 +1,56 @@ +//! Example: Reducer Pattern +//! ----------------- +//! This example shows how to encapsulate sate in dioxus components with the reducer pattern. +//! This pattern is very useful when a single component can handle many types of input that can +//! be represented by an enum. + +fn main() {} +use dioxus::prelude::*; + +pub static ExampleReducer: FC<()> = |ctx| { + let (state, reduce) = use_reducer(&ctx, PlayerState::new, PlayerState::reduce); + + let is_playing = state.is_playing(); + + ctx.render(rsx! { + div { + h1 {"Select an option"} + h3 {"The radio is... {is_playing}!"} + button { + "Pause" + onclick: move |_| reduce(PlayerAction::Pause) + } + button { + "Play" + onclick: move |_| reduce(PlayerAction::Play) + } + } + }) +}; + +enum PlayerAction { + Pause, + Play, +} + +struct PlayerState { + is_playing: bool, +} + +impl PlayerState { + fn new() -> Self { + Self { is_playing: false } + } + fn reduce(&mut self, action: PlayerAction) { + match action { + PlayerAction::Pause => self.is_playing = false, + PlayerAction::Play => self.is_playing = true, + } + } + fn is_playing(&self) -> &'static str { + match self.is_playing { + true => "currently playing!", + false => "not currently playing", + } + } +} diff --git a/packages/core-macro/examples/prop_test.rs b/packages/core-macro/examples/prop_test.rs index da4d7e961..9a38fd604 100644 --- a/packages/core-macro/examples/prop_test.rs +++ b/packages/core-macro/examples/prop_test.rs @@ -2,18 +2,21 @@ fn main() {} pub mod dioxus { pub mod prelude { - pub unsafe trait Properties { + pub trait Properties { type Builder; - const CAN_BE_MEMOIZED: bool; fn builder() -> Self::Builder; + unsafe fn memoize(&self, other: &Self) -> bool; } } } -#[derive(dioxus_core_macro::Props)] + +/// This implementation should require a "PartialEq" because it memoizes (no external references) +#[derive(PartialEq, dioxus_core_macro::Props)] struct SomeProps { a: String, } +/// This implementation does not require a "PartialEq" because it does not memoize #[derive(dioxus_core_macro::Props)] struct SomePropsTwo<'a> { a: &'a str, diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index 1f400c41b..f1f0ed4b4 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -44,11 +44,21 @@ pub enum VirtualEvent { MouseEvent(Rc), PointerEvent(Rc), + // image event has conflicting method types // ImageEvent(event_data::ImageEvent), OtherEvent, } pub mod on { + //! This module defines the synthetic events that all Dioxus apps enable. No matter the platform, every dioxus renderer + //! will implement the same events and same behavior (bubbling, cancelation, etc). + //! + //! Synthetic events are immutable and wrapped in Arc. It is the intention for Dioxus renderers to re-use the underyling + //! Arc allocation through "get_mut" + //! + //! + //! + #![allow(unused)] use std::{fmt::Debug, ops::Deref, rc::Rc}; @@ -178,8 +188,8 @@ pub mod on { pub trait MouseEvent: Debug { fn alt_key(&self) -> bool; - fn button(&self) -> i32; - fn buttons(&self) -> i32; + fn button(&self) -> i16; + fn buttons(&self) -> u16; fn client_x(&self) -> i32; fn client_y(&self) -> i32; fn ctrl_key(&self) -> bool; @@ -189,7 +199,7 @@ pub mod on { fn screen_x(&self) -> i32; fn screen_y(&self) -> i32; fn shift_key(&self) -> bool; - fn get_modifier_state(&self, key_code: usize) -> bool; + fn get_modifier_state(&self, key_code: &str) -> bool; } pub trait PointerEvent: Debug { diff --git a/packages/core/src/nodebuilder.rs b/packages/core/src/nodebuilder.rs index c3ea6e93a..6b526f37b 100644 --- a/packages/core/src/nodebuilder.rs +++ b/packages/core/src/nodebuilder.rs @@ -504,9 +504,9 @@ where let child = item.into_vnode(&self.ctx); self.children.push(child); } - if self.children.len() > len_before + 1 { - if self.children.last().unwrap().key().is_none() { - if cfg!(debug_assertions) { + if cfg!(debug_assertions) { + if self.children.len() > len_before + 1 { + if self.children.last().unwrap().key().is_none() { log::error!( r#" Warning: Each child in an array or iterator should have a unique "key" prop. diff --git a/packages/html-namespace/examples/poc.rs b/packages/html-namespace/examples/poc.rs new file mode 100644 index 000000000..acd882e1d --- /dev/null +++ b/packages/html-namespace/examples/poc.rs @@ -0,0 +1,30 @@ +//! POC: Planning the layout of a single element type +//! +//! +//! The ultimate goal with a dedicated namespace is three-fold: +//! - compile-time correct templates preventing misuse of elemnents +//! - deep integration of DSL with IDE +//! +//! +//! + +struct NodeCtx {} + +struct div<'a>(&NodeCtx); +impl<'a> div<'a> { + fn new(cx: &NodeCtx) -> Self { + div(cx) + } +} + +fn main() {} + +fn factory( + // this is your mom + cx: &NodeCtx, +) { + div::new(cx); + rsx! { + div {} + } +} diff --git a/packages/web/src/new.rs b/packages/web/src/new.rs index 5c79dea25..b4c874002 100644 --- a/packages/web/src/new.rs +++ b/packages/web/src/new.rs @@ -480,43 +480,46 @@ fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { pub struct CustomMouseEvent(web_sys::MouseEvent); impl dioxus_core::events::on::MouseEvent for CustomMouseEvent { fn alt_key(&self) -> bool { - todo!() + self.0.alt_key() } - fn button(&self) -> i32 { - todo!() + fn button(&self) -> i16 { + self.0.button() } - fn buttons(&self) -> i32 { - todo!() + fn buttons(&self) -> u16 { + self.0.buttons() } fn client_x(&self) -> i32 { - todo!() + self.0.client_x() } fn client_y(&self) -> i32 { - todo!() + self.0.client_y() } fn ctrl_key(&self) -> bool { - todo!() + self.0.ctrl_key() } fn meta_key(&self) -> bool { - todo!() + self.0.meta_key() } fn page_x(&self) -> i32 { - todo!() + self.0.page_x() } fn page_y(&self) -> i32 { - todo!() + self.0.page_y() } fn screen_x(&self) -> i32 { - todo!() + self.0.screen_x() } fn screen_y(&self) -> i32 { - todo!() + self.0.screen_y() } fn shift_key(&self) -> bool { - todo!() + self.0.shift_key() } - fn get_modifier_state(&self, key_code: usize) -> bool { - todo!() + + // yikes + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values + fn get_modifier_state(&self, key_code: &str) -> bool { + self.0.get_modifier_state(key_code) } } VirtualEvent::MouseEvent(Rc::new(CustomMouseEvent(evt))) diff --git a/src/lib.rs b/src/lib.rs index 439aace83..242d71720 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -182,12 +182,14 @@ pub mod prelude { pub use dioxus_core::prelude::*; pub use dioxus_core_macro::fc; } -pub mod builder { - // pub use dioxus_core::builder::*; -} -pub mod events { - // pub use dioxus_core::events::*; -} +// pub mod builder { +// // pub use dioxus_core::builder::*; +// } +pub use dioxus_core::builder; +pub use dioxus_core::events; +// pub mod events { +// // pub use dioxus_core::events::*; +// } // Just a heads-up, the core functionality of dioxus rests in Dioxus-Core. This crate just wraps a bunch of utilities // together and exports their namespaces to something predicatble. #[cfg(feature = "core")] From de1535ddac4d313ba9bd6f8b8d662b6b2a7e0309 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Wed, 23 Jun 2021 23:25:34 -0400 Subject: [PATCH 09/20] wip: more anitpatterns --- examples/antipatterns.rs | 84 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 78 insertions(+), 6 deletions(-) diff --git a/examples/antipatterns.rs b/examples/antipatterns.rs index 69562bbde..5091aeab1 100644 --- a/examples/antipatterns.rs +++ b/examples/antipatterns.rs @@ -5,9 +5,17 @@ //! anti-patterns are considered wrong to due performance reasons or violate the "rules" of Dioxus. These rules are //! borrowed from other successful UI frameworks, and Dioxus is more focused on providing a familiar, ergonomic interface //! rather than building new harder-to-misuse patterns. -use std::collections::HashMap; - +//! +//! In this list we showcase: +//! - Not adding keys for iterators +//! - Heavily nested fragments +//! - Understadning ordering of set_state +//! - Naming conventions +//! - Rules of hooks +//! +//! Feel free to file a PR or Issue if you run into another antipattern that you think users of Dioxus should know about. use dioxus::prelude::*; + fn main() {} /// Antipattern: Iterators without keys @@ -15,12 +23,16 @@ fn main() {} /// /// This is considered an anti-pattern for performance reasons. Dioxus must diff your current and old layout and must /// take a slower path if it can't correlate old elements with new elements. Lists are particularly susceptible to the -/// "slow" path, so you're strongly encouraged to provide a unique ID stable between renders. +/// "slow" path, so you're strongly encouraged to provide a unique ID stable between renders. Additionally, providing +/// the *wrong* keys is even worse. Keys should be: +/// - Unique +/// - Stable +/// - Predictable /// /// Dioxus will log an error in the console if it detects that your iterator does not properly generate keys #[derive(PartialEq, Props)] struct NoKeysProps { - data: HashMap, + data: std::collections::HashMap, } static AntipatternNoKeys: FC = |cx| { // WRONG: Make sure to add keys! @@ -44,8 +56,8 @@ static AntipatternNoKeys: FC = |cx| { /// /// Only Component and Fragment nodes are susceptible to this issue. Dioxus mitigates this with components by providing /// an API for registering shared state without the ContextProvider pattern. -static Blah: FC<()> = |cx| { - // Try to avoid +static AntipatternNestedFragments: FC<()> = |cx| { + // Try to avoid heavily nesting fragments rsx!(in cx, Fragment { Fragment { @@ -60,3 +72,63 @@ static Blah: FC<()> = |cx| { } ) }; + +/// Antipattern: Using state after its been updated +/// ----------------------------------------------- +/// +/// This is an antipattern in other frameworks, but less so in Dioxus. However, it's important to highlight that use_state +/// does *not* work the same way as it does in React. Rust provides explicit guards against mutating shared data - a huge +/// problem in JavaScript land. With Rust and Dioxus, it's nearly impossible to misuse `use_state` - you simply can't +/// accidentally modify the state you've received! +/// +/// However, calling set_state will *not* update the current version of state in the component. This should be easy to +/// recognize from the function signature, but Dioxus will not update the "live" version of state. Calling `set_state` +/// merely places a new value in the queue and schedules the component for a future update. +static AntipaternRelyingOnSetState: FC<()> = |cx| { + let (state, set_state) = use_state(&cx, || "Hello world"); + set_state("New state"); + // This will return false! `state` will *still* be "Hello world" + assert!(state == &"New state"); + todo!() +}; + +/// Antipattern: Capitalization +/// --------------------------- +/// +/// This antipattern is enforced to retain parity with other frameworks and provide useful IDE feedback, but is less +/// critical than other potential misues. In short: +/// - Only raw elements may start with a lowercase character +/// - All components must start with an uppercase character +/// +/// IE: the following component will be rejected when attempted to be used in the rsx! macro +static antipattern_component: FC<()> = |cx| todo!(); + +/// Antipattern: Misusing hooks +/// --------------------------- +/// +/// This pattern is an unfortunate one where Dioxus supports the same behavior as in other frameworks. Dioxus supports +/// "hooks" - IE "memory cells" that allow a value to be stored between renders. This allows other hooks to tap into +/// a components "memory" without explicitly adding all of its data to a struct definition. In Dioxus, hooks are allocated +/// with a bump arena and then immediately sealed. +/// +/// This means that hooks may not be misued: +/// - Called out of order +/// - Called in a conditional +/// - Called in loops or callbacks +/// +/// For the most part, Rust helps with rule #3 but does not save you from misusing rule #1 or #2. Dioxus will panic +/// if hooks do not downcast the same data between renders. This is validated by TypeId - and eventually - a custom key. +#[derive(PartialEq, Props)] +struct MisuedHooksProps { + should_render_state: bool, +} +static AntipatternMisusedHooks: FC = |cx| { + if cx.should_render_state { + // do not place a hook in the conditional! + // prefer to move it out of the conditional + let (state, set_state) = use_state(&cx, || "hello world"); + rsx!(in cx, div { "{state}" }) + } else { + rsx!(in cx, div { "Not rendering state" }) + } +}; From 17f6576e9808c4985be9a029bf61c3acbf11e61d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 24 Jun 2021 00:18:29 -0400 Subject: [PATCH 10/20] Docs: fleshed out custom renderer doc --- .vscode/spellright.dict | 1 + docs/advanced-guides/custom-renderer.md | 168 ++++++++++++++++++ .../liveview.md} | 0 docs/posts/08-custom-renderer.md | 30 ---- examples/antipatterns.rs | 42 +++++ 5 files changed, 211 insertions(+), 30 deletions(-) create mode 100644 docs/advanced-guides/custom-renderer.md rename docs/{posts/12-liveview.md => advanced-guides/liveview.md} (100%) delete mode 100644 docs/posts/08-custom-renderer.md diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index 3a762ee1d..ee02bfa75 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -57,3 +57,4 @@ dom textarea noderefs wasm +7ns diff --git a/docs/advanced-guides/custom-renderer.md b/docs/advanced-guides/custom-renderer.md new file mode 100644 index 000000000..f689daffb --- /dev/null +++ b/docs/advanced-guides/custom-renderer.md @@ -0,0 +1,168 @@ +# Custom Renderer + +Dioxus is an incredibly portable framework for UI development. The lessons, knowledge, hooks, and components you acquire over time can always be used for future projects. However, sometimes those projects cannot leverage a supported renderer or you need to implement your own better renderer. + +Great news: the design of the renderer is entirely up to you! We provide suggestions and inspiration with the 1st party renderers, but provide no trait or explicit interface to follow. + +## The specifics: + +Implementing the renderer is fairly straightforward. The renderer needs to: + +1. Handle the stream of edit events generated by updates to the virtual DOM +2. Register listeners and pass events into the virtual DOM's event system +3. Progress the virtual DOM with an async executor (or disable the suspense API and use `progress_sync`) + +Essentially, your renderer needs to implement the `RealDom` trait and generate `EventTrigger` objects to update the VirtualDOM. From there, you'll have everything needed to render the VirtualDOM to the screen. + +Internally, Dioxus handles the tree relationship, diffing, memory management, and the event system, leaving as little as possible required for renderers to implement themselves. + +For reference, check out the WebSys renderer as a starting point for your custom renderer. + +## Trait implementation + +The current `RealDom` trait lives in `dioxus_core/diff`. A version of it is provided here: + +```rust +pub trait RealDom { + // Navigation + fn push_root(&mut self, root: RealDomNode); + + // Add Nodes to the dom + fn append_child(&mut self); + fn replace_with(&mut self); + + // Remove Nodesfrom the dom + fn remove(&mut self); + fn remove_all_children(&mut self); + + // Create + fn create_text_node(&mut self, text: &str) -> RealDomNode; + fn create_element(&mut self, tag: &str) -> RealDomNode; + fn create_element_ns(&mut self, tag: &str, namespace: &str) -> RealDomNode; + + // Events + fn new_event_listener( + &mut self, + event: &str, + scope: ScopeIdx, + element_id: usize, + realnode: RealDomNode, + ); + fn remove_event_listener(&mut self, event: &str); + + // modify + fn set_text(&mut self, text: &str); + fn set_attribute(&mut self, name: &str, value: &str, is_namespaced: bool); + fn remove_attribute(&mut self, name: &str); + + // node ref + fn raw_node_as_any_mut(&self) -> &mut dyn Any; +} +``` + +This trait defines what the Dioxus VirtualDOM expects a "RealDom" abstraction to implement for the Dioxus diffing mechanism to function properly. The Dioxus diffing mechanism operates as a [stack machine](https://en.wikipedia.org/wiki/Stack_machine) where the "push_root" method pushes a new "real" DOM node onto the stack and "append_child" and "replace_with" both remove nodes from the stack. When the RealDOM creates new nodes, it must return the `RealDomNode` type... which is just an abstraction over u32. We strongly recommend the use of `nohash-hasher`'s IntMap for managing the mapping of `RealDomNode` (ids) to their corresponding real node. For an IntMap of 1M+ nodes, an index time takes about 7ns which is very performant when compared to the traditional hasher. + +## Event loop + +Like most GUIs, Dioxus relies on an event loop to progress the VirtualDOM. The VirtualDOM itself can produce events as well, so it's important that your custom renderer can handle those too. + +The code for the WebSys implementation is straightforward, so we'll add it here to demonstrate how simple an event loop is: + +```rust +pub async fn run(&mut self) -> dioxus_core::error::Result<()> { + // Push the body element onto the WebsysDom's stack machine + let mut websys_dom = crate::new::WebsysDom::new(prepare_websys_dom().first_child().unwrap()); + websys_dom.stack.push(root_node); + + // Rebuild or hydrate the virtualdom + self.internal_dom.rebuild(&mut websys_dom)?; + + // Wait for updates from the real dom and progress the virtual dom + while let Some(trigger) = websys_dom.wait_for_event().await { + websys_dom.stack.push(body_element.first_child().unwrap()); + self.internal_dom + .progress_with_event(&mut websys_dom, trigger)?; + } +} +``` + +It's important that you decode the real events from your event system into Dioxus' synthetic event system (synthetic meaning abstracted). This simply means matching your event type and creating a Dioxus `VirtualEvent` type. Your custom event must implement the corresponding event trait: + +```rust +fn virtual_event_from_websys_event(event: &web_sys::Event) -> VirtualEvent { + match event.type_().as_str() { + "keydown" | "keypress" | "keyup" => { + struct CustomKeyboardEvent(web_sys::KeyboardEvent); + impl dioxus::events::KeyboardEvent for CustomKeyboardEvent { + fn char_code(&self) -> usize { self.0.char_code() } + fn ctrl_key(&self) -> bool { self.0.ctrl_key() } + fn key(&self) -> String { self.0.key() } + fn key_code(&self) -> usize { self.0.key_code() } + fn locale(&self) -> String { self.0.locale() } + fn location(&self) -> usize { self.0.location() } + fn meta_key(&self) -> bool { self.0.meta_key() } + fn repeat(&self) -> bool { self.0.repeat() } + fn shift_key(&self) -> bool { self.0.shift_key() } + fn which(&self) -> usize { self.0.which() } + fn get_modifier_state(&self, key_code: usize) -> bool { self.0.get_modifier_state() } + } + VirtualEvent::KeyboardEvent(Rc::new(event.clone().dyn_into().unwrap())) + } + _ => todo!() +``` + +## Custom raw elements + +If you need to go as far as relying on custom elements for your renderer - you totally can. This still enables you to use Dioxus' reactive nature, component system, shared state, and other features, but will ultimately generate different nodes. All attributes and listeners for the HTML and SVG namespace are shuttled through helper structs that essentially compile away (pose no runtime overhead). You can drop in your own elements any time you want, with little hassle. However, you must be absolutely sure your renderer can handle the new type, or it will crash and burn. + +For example, the `div` element is (approximately!) defined as such: + +```rust +struct div(NodeBuilder); +impl<'a> div { + #[inline] + fn new(factory: &NodeFactory<'a>) -> Self { + Self(factory.new_element("div")) + } + #[inline] + fn onclick(mut self, callback: impl Fn(MouseEvent) + 'a) -> Self { + self.0.add_listener("onclick", |evt: VirtualEvent| match evt { + MouseEvent(evt) => callback(evt), + _ => {} + }); + self + } + // etc +} +``` + +The rsx! and html! macros just use the `div` struct as a compile-time guard around the NodeFactory. + +## Compatibility + +Forewarning: not every hook and service will work on your platform. Dioxus wraps things that need to be cross-platform in "synthetic" types. However, downcasting to a native type might fail if the types don't match. + +There are three opportunities for platform incompatibilities to break your program: + +1. When downcasting elements via `Ref.to_native()` +2. When downcasting events via `Event.to_native()` +3. Calling platform-specific APIs that don't exist + +The best hooks will properly detect the target platform and still provide functionality, failing gracefully when a platform is not supported. We encourage - and provide - an indication to the user on what platforms a hook supports. For issues 1 and 2, these return a result as to not cause panics on unsupported platforms. When designing your hooks, we recommend propagating this error upwards into user facing code, making it obvious that this particular service is not supported. + +This particular code _will panic_ due to the unwrap. Try to avoid these types of patterns. + +```rust +let div_ref = use_node_ref(&cx); + +cx.render(rsx!{ + div { ref: div_ref, class: "custom class", + button { "click me to see my parent's class" + onclick: move |_| if let Some(div_ref) = div_ref { + panic!("Div class is {}", div_ref.to_native::().unwrap().class()) + } + } + } +}) + +``` diff --git a/docs/posts/12-liveview.md b/docs/advanced-guides/liveview.md similarity index 100% rename from docs/posts/12-liveview.md rename to docs/advanced-guides/liveview.md diff --git a/docs/posts/08-custom-renderer.md b/docs/posts/08-custom-renderer.md deleted file mode 100644 index 346c26f73..000000000 --- a/docs/posts/08-custom-renderer.md +++ /dev/null @@ -1,30 +0,0 @@ -# Custom Renderer - -Dioxus is an incredibly portable framework for UI development. The lessons, knowledge, hooks, and components you acquire over time can always be used for future projects. However, sometimes those projects cannot leverage a supported renderer or you need to implement your own better renderer. - -Great news: the design of the renderer is entirely up to you! We provide suggestions and inspiration with the 1st party renderers, but provide no trait or explicit interface to follow. - -Implementing the renderer is fairly straightforward. The renderer needs to: -1. Handle the stream of edit events generated by updates to the virtual DOM -2. Register listeners and pass events into the virtual DOM's event system -3. Progress the virtual DOM with an async executor (or disable the suspense API and use `progress_sync`) - -Essentially, your renderer needs to understand the EditEvent type and provide a callback for injecting events. From there, you'll have everything needed to render the VirtualDOM to the screen. - -Internally, Dioxus handles the tree relationship, diffing, memory management, and the event system, leaving as little as possible required for renderers to implement themselves. - -For inspiration, check out the source code for the various renderers we support: -- WebSys -- Morph - -## Compatibility - -Forewarning: not every hook and service will work on your platform. Dioxus wraps things that need to be cross-platform in "synthetic" types. However, downcasting to a native type might fail if the types don't match. - -There are three opportunities for platform incompatibilities to break your program: -1. When downcasting elements via `Ref.to_native()` -2. When downcasting events via `Event.to_native()` -3. Calling platform-specific APIs that don't exist - -The best hooks will properly detect the target platform and still provide functionality, failing gracefully when a platform is not supported. We encourage - and provide - an indication to the user on what platforms a hook supports. For issues 1 and 2, these return a result as to not cause panics on unsupported platforms. When designing your hooks, we recommend propagating this error upwards into user facing code, making it obvious that this particular service is not supported. - diff --git a/examples/antipatterns.rs b/examples/antipatterns.rs index 5091aeab1..e562c093b 100644 --- a/examples/antipatterns.rs +++ b/examples/antipatterns.rs @@ -132,3 +132,45 @@ static AntipatternMisusedHooks: FC = |cx| { rsx!(in cx, div { "Not rendering state" }) } }; + +/// Antipattern: Downcasting refs and panicing +/// ------------------------------------------ +/// +/// Occassionally it's useful to get the ref of an element to handle it directly. Elements support downcasting to +/// Dioxus's virtual element types as well as their true native counterparts. Downcasting to Dioxus' virtual elements +/// will never panic, but downcasting to native elements will fail if on an unsupported platform. We recommend avoiding +/// publishing hooks and components that deply rely on control over elements using their native `ref`, preferring to +/// use their Dioxus Virtual Element counterpart instead. +// This particular code *will panic* due to the unwrap. Try to avoid these types of patterns. +/// --------------------------------- +/// TODO: Get this to compile properly +/// let div_ref = use_node_ref(&cx); +/// +/// cx.render(rsx!{ +/// div { ref: div_ref, class: "custom class", +/// button { "click me to see my parent's class" +/// onclick: move |_| if let Some(div_ref) = div_ref { +/// panic!("Div class is {}", div_ref.to_native::().unwrap().class()) +/// } +/// } +/// } +/// }) +static _example: FC<()> = |cx| todo!(); + +/// Antipattern: publishing components and hooks with all features enabled +/// ---------------------------------------------------------------------- +/// +/// The `dioxus` crate combines a bunch of useful utilities together (like the rsx! and html! macros, hooks, and more). +/// However, when publishing your custom hook or component, we highly advise using only the `core` feature on the dioxus +/// crate. This makes your crate compile faster, makes it more stable, and avoids bringing in incompatible libraries that +/// might make it not compile on unsupported platforms. +/// +/// We don't have a code snippet for this, but just prefer to use this line: +/// dioxus = { version = "*", features = ["core"]} +/// instead of this one: +/// dioxus = { version = "*", features = ["web", "desktop", "full"]} +/// in your Cargo.toml +/// +/// This will only include the `core` dioxus crate which is relatively slim and fast to compile and avoids target-specific +/// libraries. +static __example: FC<()> = |cx| todo!(); From 100a78f321a47a97c1a6a3fc2d51d8f70c797974 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 24 Jun 2021 00:27:48 -0400 Subject: [PATCH 11/20] docs: fix types and add a conclusion to custom renderer guide --- docs/advanced-guides/custom-renderer.md | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/advanced-guides/custom-renderer.md b/docs/advanced-guides/custom-renderer.md index f689daffb..af5513dfc 100644 --- a/docs/advanced-guides/custom-renderer.md +++ b/docs/advanced-guides/custom-renderer.md @@ -2,13 +2,13 @@ Dioxus is an incredibly portable framework for UI development. The lessons, knowledge, hooks, and components you acquire over time can always be used for future projects. However, sometimes those projects cannot leverage a supported renderer or you need to implement your own better renderer. -Great news: the design of the renderer is entirely up to you! We provide suggestions and inspiration with the 1st party renderers, but provide no trait or explicit interface to follow. +Great news: the design of the renderer is entirely up to you! We provide suggestions and inspiration with the 1st party renderers, but only really require implementation of the `RealDom` trait for things to function properly. ## The specifics: Implementing the renderer is fairly straightforward. The renderer needs to: -1. Handle the stream of edit events generated by updates to the virtual DOM +1. Handle the stream of edits generated by updates to the virtual DOM 2. Register listeners and pass events into the virtual DOM's event system 3. Progress the virtual DOM with an async executor (or disable the suspense API and use `progress_sync`) @@ -31,14 +31,13 @@ pub trait RealDom { fn append_child(&mut self); fn replace_with(&mut self); - // Remove Nodesfrom the dom + // Remove Nodes from the dom fn remove(&mut self); fn remove_all_children(&mut self); // Create fn create_text_node(&mut self, text: &str) -> RealDomNode; - fn create_element(&mut self, tag: &str) -> RealDomNode; - fn create_element_ns(&mut self, tag: &str, namespace: &str) -> RealDomNode; + fn create_element(&mut self, tag: &str, namespace: Option<&str>) -> RealDomNode; // Events fn new_event_listener( @@ -166,3 +165,7 @@ cx.render(rsx!{ }) ``` + +## Conclusion + +That should be it! You should have nearly all the knowledge required on how to implement your own renderer. We're super interested in seeing Dioxus apps brought to custom desktop renderers, mobile renderer, video game UI, and even augmented reality! If you're interesting in contributing to any of the these projects, don't be afraid to reach out or join the community. From 7fbaf69cabbdde712bb3fd9e4b2a5dc18b9390e9 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 24 Jun 2021 11:09:38 -0400 Subject: [PATCH 12/20] Docs: more examples and docs --- README.md | 41 ++++++++++ examples/listener.rs | 8 +- examples/rsx_usage.rs | 124 +++++++++++++++++++++++++++---- examples/webview.rs | 32 ++++---- notes/Parity.md | 3 +- packages/core/src/virtual_dom.rs | 2 +- packages/web/Cargo.toml | 16 ---- packages/web/examples/context.rs | 1 + src/lib.rs | 4 +- 9 files changed, 177 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 98a156194..321940025 100644 --- a/README.md +++ b/README.md @@ -89,3 +89,44 @@ TypeScript is a great addition to JavaScript, but comes with a lot of tweaking f - various macros (`html!`, `rsx!`) for fast template iteration And much more. Dioxus makes Rust apps just as fast to write as React apps, but affords more robustness, giving your frontend team greater confidence in making big changes in shorter time. Dioxus also works on the server, on the web, on mobile, on desktop - and it runs completely natively so performance is never an issue. + +# Parity with React + +Sorted by priority + +| Feature | Dioxus | React | Notes | +| ---------------------- | ------ | ----- | ------------------------------------------------ | +| ----- Phase 1 ----- | ----- | ----- | ----- | +| Conditional Rendering | ✅ | ✅ | if/then to hide/show component | +| Map, Iterator | ✅ | ✅ | map/filter/reduce rsx! | +| Keyed Components | ✅ | ✅ | advanced diffing with keys | +| Web | ✅ | ✅ | renderer for web browser | +| Desktop (webview) | ✅ | ✅ | renderer for desktop | +| Context | ✅ | ✅ | share state through the tree | +| Hook | ✅ | ✅ | memory cells in components | +| SSR | ✅ | ✅ | render directly to string | +| Runs natively | ✅ | 👀 | runs as a portable binary w/o a runtime (Node) | +| Component Children | ✅ | ✅ | cx.children() as a list of nodes | +| Null components | ✅ | ✅ | allow returning no components | +| No-div components | ✅ | ✅ | components that render components | +| Fragments | ✅ | ✅ | rsx! can return multiple elements without a root | +| Manual Props | 👀 | ✅ | Manually pass in props | +| NodeRef | 👀 | ✅ | gain direct access to nodes | +| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | +| CSS/Inline Styles | 🛠 | ✅ | syntax for inline/conditional styles | +| 1st class global state | 🛠 | ✅ | redux/recoil/mobx on top of context | +| ----- Phase 2 ----- | ----- | ----- | ----- | +| 1st class router | 👀 | ✅ | Hook built on top of history | +| Assets | 👀 | ✅ | include css/svg/img url statically | +| Integrated classnames | 🛠 | 👀 | built-in `classnames` | +| Suspense | 👀 | 👀 | schedule future render from future/promise | +| Transition | 👀 | 👀 | High-level control over suspense | +| Animation | 👀 | ✅ | Spring-style animations | +| Mobile | 👀 | ✅ | Render with cacao | +| Desktop (native) | 👀 | ✅ | Render with native desktop | +| 3D Renderer | 👀 | ✅ | react-three-fiber | +| ----- Phase 3 ----- | ----- | ----- | ----- | +| Portal | 👀 | ✅ | cast elements through tree | +| Error/Panic boundary | 👀 | ✅ | catch panics and display custom BSOD | +| Code-splitting | 👀 | ✅ | Make bundle smaller/lazy | +| LiveView | 👀 | 👀 | Example for SSR + WASM apps | diff --git a/examples/listener.rs b/examples/listener.rs index 6bd7f8df9..5db32f799 100644 --- a/examples/listener.rs +++ b/examples/listener.rs @@ -2,13 +2,7 @@ use dioxus_core::prelude::*; -fn main() { - Some(10) - .map(|f| f * 5) - .map(|f| f / 3) - .map(|f| f * 5) - .map(|f| f / 3); -} +fn main() {} static Example: FC<()> = |ctx| { let (name, set_name) = use_state(&ctx, || "...?"); diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index 938bd6622..da3d77eba 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -5,10 +5,6 @@ //! //! A full in-depth reference guide is available at: https://www.notion.so/rsx-macro-basics-ef6e367dec124f4784e736d91b0d0b19 //! -//! ## Topics -//! -//! -//! //! ### Elements //! - Create any element from its tag //! - Accept compile-safe attributes for each tag @@ -46,23 +42,119 @@ fn main() { dioxus::webview::launch(Example); } +/// When trying to return "nothing" to Dioxus, you'll need to specify the type parameter or Rust will be sad. +/// This type alias specifices the type for you so you don't need to write "None as Option<()>" +const NONE_ELEMENT: Option<()> = None; + use baller::Baller; use dioxus_core::prelude::*; -static Example: FC<()> = |ctx| { - ctx.render(rsx! { +static Example: FC<()> = |cx| { + let formatting = "formatting!"; + let formatting_tuple = ("a", "b"); + let lazy_fmt = format_args!("lazily formatted text"); + cx.render(rsx! { div { // Elements + div {} + h1 {"Some text"} + h1 {"Some text with {formatting}"} + h1 {"Formatting basic expressions {formatting_tuple.0} and {formatting_tuple.1}"} + h2 { + "Multiple" + "Text" + "Blocks" + "Use comments as separators in html" + } + div { + h1 {"multiple"} + h2 {"nested"} + h3 {"elements"} + } + div { + class: "my special div" + h1 {"Headers and attributes!"} + } + div { + // pass simple rust expressions in + class: lazy_fmt, + id: format_args!("attributes can be passed lazily with std::fmt::Arguments"), + div { + class: { + const WORD: &str = "expressions"; + format_args!("Arguments can be passed in through curly braces for complex {}", WORD) + } + } + } + // Expressions can be used in element position too: + {rsx!(p { "More templating!" })} + {html!(

"Even HTML templating!!"

)} + // Iterators + {(0..10).map(|i| rsx!(li { "{i}" }))} + {{ + let data = std::collections::HashMap::<&'static str, &'static str>::new(); + // Iterators *should* have keys when you can provide them. + // Keys make your app run faster. Make sure your keys are stable, unique, and predictable. + // Using an "ID" associated with your data is a good idea. + data.into_iter().map(|(k, v)| rsx!(li { key: "{k}" "{v}" })) + }} + + // Matching + // Matching will throw a Rust error about "no two closures are the same type" + // To fix this, call "render" method or use the "in" syntax to produce VNodes. + // There's nothing we can do about it, sorry :/ (unless you want *really* unhygenic macros) + {match true { + true => rsx!(in cx, h1 {"Top text"}), + false => cx.render(rsx!( h1 {"Bottom text"})) + }} - // ============== - // Components - // ============== + // Conditional rendering + // Dioxus conditional rendering is based around None/Some. We have no special syntax for conditionals. + // You can convert a bool condition to rsx! with .then and .or + {true.then(|| rsx!(div {}))} + + // True conditions need to be rendered (same reasons as matching) + {if true { + rsx!(in cx, h1 {"Top text"}) + } else { + cx.render(rsx!( h1 {"Bottom text"})) + }} + + // returning "None" is a bit noisy... but rare in practice + {None as Option<()>} + + // Use the Dioxus type-alias for less noise + {NONE_ELEMENT} + + // can also just use empty fragments + Fragment {} + + // Fragments let you insert groups of nodes without a parent. + // This lets you make components that insert elements as siblings without a container. + div {"A"} + Fragment { + div {"B"} + div {"C"} + Fragment { + "D" + Fragment { + "heavily nested fragments is an antipattern" + "they cause Dioxus to do unnecessary work" + "don't use them carelessly if you can help it" + } + } + } + + + // Components // Can accept any paths - crate::baller::Baller {} + // Notice how you still get syntax highlighting and IDE support :) + Baller {} baller::Baller { } + crate::baller::Baller {} // Can take properties Taller { a: "asd" } @@ -71,10 +163,14 @@ static Example: FC<()> = |ctx| { Taller { a: "asd" } // Can pass in props directly - Taller { a: "asd" /* ..{props}*/ } + {{ + todo!("this neesd to be implemented"); + let props = TallerProps {a: "hello"}; + rsx!(Taller {a: "a"}) + }} // Can take children - Taller { a: "asd", div {} } + Taller { a: "asd", div {"hello world!"} } } }) }; @@ -83,7 +179,8 @@ mod baller { use super::*; pub struct BallerProps {} - pub fn Baller(ctx: Context<()>) -> VNode { + /// This component totally balls + pub fn Baller(cx: Context<()>) -> VNode { todo!() } } @@ -93,6 +190,7 @@ pub struct TallerProps { a: &'static str, } +/// This component is taller than most :) pub fn Taller(ctx: Context) -> VNode { let b = true; todo!() diff --git a/examples/webview.rs b/examples/webview.rs index 22053dc96..c317aef2a 100644 --- a/examples/webview.rs +++ b/examples/webview.rs @@ -6,22 +6,26 @@ //! Under the hood, the dioxus_webview crate bridges a native Dioxus VirtualDom with a custom prebuit application running //! in the webview runtime. Custom handlers are provided for the webview instance to consume patches and emit user events //! into the native VDom instance. +//! +//! Currently, NodeRefs won't work properly, but all other event functionality will. use dioxus::prelude::*; fn main() { - dioxus::webview::launch(|ctx| { - let (count, set_count) = use_state(&ctx, || 0); - - ctx.render(rsx! { - div { - h1 { "Dioxus Desktop Demo" } - p { "Count is {count}" } - button { - "Click to increment" - onclick: |_| set_count(count + 1) - } - } - }) - }); + dioxus::webview::launch(App); } + +static App: FC<()> = |cx| { + let (count, set_count) = use_state(&cx, || 0); + + cx.render(rsx! { + div { + h1 { "Dioxus Desktop Demo" } + p { "Count is {count}" } + button { + "Click to increment" + onclick: move |_| set_count(count + 1) + } + } + }) +}; diff --git a/notes/Parity.md b/notes/Parity.md index e1a593859..c838aedbb 100644 --- a/notes/Parity.md +++ b/notes/Parity.md @@ -13,11 +13,12 @@ Sorted by priority | Context | ✅ | ✅ | share state through the tree | | Hook | ✅ | ✅ | memory cells in components | | SSR | ✅ | ✅ | render directly to string | -| Runs natively | ✅ | 👀 | runs as a portable binary w/ extra tooling | +| Runs natively | ✅ | 👀 | runs as a portable binary w/o a runtime (Node) | | Component Children | ✅ | ✅ | cx.children() as a list of nodes | | Null components | ✅ | ✅ | allow returning no components | | No-div components | ✅ | ✅ | components that render components | | Fragments | ✅ | ✅ | rsx! can return multiple elements without a root | +| Manual Props | 👀 | ✅ | Manually pass in props | | NodeRef | 👀 | ✅ | gain direct access to nodes | | Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | | CSS/Inline Styles | 🛠 | ✅ | syntax for inline/conditional styles | diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 55f80c4f9..9b43779f5 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -778,7 +778,7 @@ Any function prefixed with "use" should not be called conditionally. } /// There are hooks going on here! - fn use_context(&self) -> &'src Rc { + fn use_context(&self) -> &'src T { self.try_use_context().unwrap() } diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index c00682739..577706d00 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -23,21 +23,15 @@ wasm-bindgen-test = "0.3.21" once_cell = "1.7.2" atoms = { path="../atoms" } -# wasm-bindgen = "0.2.70" -# futures = "0.3.12" -# html-validation = { path = "../html-validation", version = "0.1.1" } - async-channel = "1.6.1" nohash-hasher = "0.2.0" anyhow = "1.0.41" -# futures-lite = "1.11.3" [dependencies.web-sys] version = "0.3.50" features = [ "Comment", "Document", - # "DataTransfer", "Element", "HtmlElement", "HtmlInputElement", @@ -64,18 +58,12 @@ features = [ "DocumentType", "CharacterData", "HtmlOptionElement", - ] [profile.release] lto = true opt-level = 's' -# debug = true - -# [profile.release] - - [lib] crate-type = ["cdylib", "rlib"] @@ -83,7 +71,3 @@ crate-type = ["cdylib", "rlib"] im-rc = "15.0.0" rand = { version="0.8.4", features=["small_rng"] } uuid = { version="0.8.2", features=["v4", "wasm-bindgen"] } - -[[example]] -name = "todomvc" -path = "./examples/todomvc/main.rs" diff --git a/packages/web/examples/context.rs b/packages/web/examples/context.rs index 888a652d8..5db6de760 100644 --- a/packages/web/examples/context.rs +++ b/packages/web/examples/context.rs @@ -12,6 +12,7 @@ //! use dioxus_core::prelude::*; +use dioxus_core as dioxus; use dioxus_web::WebsysRenderer; fn main() { diff --git a/src/lib.rs b/src/lib.rs index 242d71720..4a01b7d9b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -231,6 +231,6 @@ pub mod atoms {} // #[cfg(feature = "desktop")] pub mod webview { //! A webview based renderer for building desktop applications with Dioxus - use dioxus_core::prelude::FC; - pub fn launch

(f: FC

) {} + use dioxus_core::prelude::{Properties, FC}; + pub fn launch(f: FC

) {} } From acbeac02d953f2c7675cf6413d49d72319b95fef Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 24 Jun 2021 11:15:25 -0400 Subject: [PATCH 13/20] docs: add a table to the readme --- README.md | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 321940025..752e19441 100644 --- a/README.md +++ b/README.md @@ -92,9 +92,11 @@ And much more. Dioxus makes Rust apps just as fast to write as React apps, but a # Parity with React +Dioxus is heavily inspired by React, but we want your transition to feel like an upgrade. Dioxus is _most_ of the way there, but missing a few key features. This parity table does not necessarily include + Sorted by priority -| Feature | Dioxus | React | Notes | +| Feature | Dioxus | React | Notes for Dioxus | | ---------------------- | ------ | ----- | ------------------------------------------------ | | ----- Phase 1 ----- | ----- | ----- | ----- | | Conditional Rendering | ✅ | ✅ | if/then to hide/show component | @@ -105,7 +107,7 @@ Sorted by priority | Context | ✅ | ✅ | share state through the tree | | Hook | ✅ | ✅ | memory cells in components | | SSR | ✅ | ✅ | render directly to string | -| Runs natively | ✅ | 👀 | runs as a portable binary w/o a runtime (Node) | +| Runs natively | ✅ | ❓ | runs as a portable binary w/o a runtime (Node) | | Component Children | ✅ | ✅ | cx.children() as a list of nodes | | Null components | ✅ | ✅ | allow returning no components | | No-div components | ✅ | ✅ | components that render components | @@ -118,15 +120,20 @@ Sorted by priority | ----- Phase 2 ----- | ----- | ----- | ----- | | 1st class router | 👀 | ✅ | Hook built on top of history | | Assets | 👀 | ✅ | include css/svg/img url statically | -| Integrated classnames | 🛠 | 👀 | built-in `classnames` | -| Suspense | 👀 | 👀 | schedule future render from future/promise | -| Transition | 👀 | 👀 | High-level control over suspense | +| Integrated classnames | 🛠 | ❓ | built-in `classnames` | +| Suspense | 👀 | 🛠 | schedule future render from future/promise | +| Transition | 👀 | 🛠 | High-level control over suspense | | Animation | 👀 | ✅ | Spring-style animations | | Mobile | 👀 | ✅ | Render with cacao | | Desktop (native) | 👀 | ✅ | Render with native desktop | | 3D Renderer | 👀 | ✅ | react-three-fiber | | ----- Phase 3 ----- | ----- | ----- | ----- | -| Portal | 👀 | ✅ | cast elements through tree | -| Error/Panic boundary | 👀 | ✅ | catch panics and display custom BSOD | +| Portal | ❓ | ✅ | cast elements through tree | +| Error/Panic boundary | ❓ | ✅ | catch panics and display custom BSOD | | Code-splitting | 👀 | ✅ | Make bundle smaller/lazy | -| LiveView | 👀 | 👀 | Example for SSR + WASM apps | +| LiveView | 👀 | ❓ | Example for SSR + WASM apps | + +✅ = implemented and working +👀 = not yet implemented or being worked on +🛠 = actively being worked on +❓ = not sure if will or can implement From 527e0abd0f88cb3ae2dc1a6ec7a62c241250444d Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 24 Jun 2021 11:15:55 -0400 Subject: [PATCH 14/20] docs: fix formatting --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 752e19441..9631e3e12 100644 --- a/README.md +++ b/README.md @@ -133,7 +133,7 @@ Sorted by priority | Code-splitting | 👀 | ✅ | Make bundle smaller/lazy | | LiveView | 👀 | ❓ | Example for SSR + WASM apps | -✅ = implemented and working -👀 = not yet implemented or being worked on -🛠 = actively being worked on -❓ = not sure if will or can implement +- ✅ = implemented and working +- 👀 = not yet implemented or being worked on +- 🛠 = actively being worked on +- ❓ = not sure if will or can implement From 772e11b9656a6a45467386913c498b674ba03921 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 24 Jun 2021 11:17:59 -0400 Subject: [PATCH 15/20] docs: split table up --- README.md | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9631e3e12..28ba8ad84 100644 --- a/README.md +++ b/README.md @@ -96,9 +96,10 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an Sorted by priority +### Phase 1: The Basics + | Feature | Dioxus | React | Notes for Dioxus | | ---------------------- | ------ | ----- | ------------------------------------------------ | -| ----- Phase 1 ----- | ----- | ----- | ----- | | Conditional Rendering | ✅ | ✅ | if/then to hide/show component | | Map, Iterator | ✅ | ✅ | map/filter/reduce rsx! | | Keyed Components | ✅ | ✅ | advanced diffing with keys | @@ -117,21 +118,29 @@ Sorted by priority | Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | | CSS/Inline Styles | 🛠 | ✅ | syntax for inline/conditional styles | | 1st class global state | 🛠 | ✅ | redux/recoil/mobx on top of context | -| ----- Phase 2 ----- | ----- | ----- | ----- | -| 1st class router | 👀 | ✅ | Hook built on top of history | -| Assets | 👀 | ✅ | include css/svg/img url statically | -| Integrated classnames | 🛠 | ❓ | built-in `classnames` | -| Suspense | 👀 | 🛠 | schedule future render from future/promise | -| Transition | 👀 | 🛠 | High-level control over suspense | -| Animation | 👀 | ✅ | Spring-style animations | -| Mobile | 👀 | ✅ | Render with cacao | -| Desktop (native) | 👀 | ✅ | Render with native desktop | -| 3D Renderer | 👀 | ✅ | react-three-fiber | -| ----- Phase 3 ----- | ----- | ----- | ----- | -| Portal | ❓ | ✅ | cast elements through tree | -| Error/Panic boundary | ❓ | ✅ | catch panics and display custom BSOD | -| Code-splitting | 👀 | ✅ | Make bundle smaller/lazy | -| LiveView | 👀 | ❓ | Example for SSR + WASM apps | + +### Phase 2: Advanced Toolkits + +| Feature | Dioxus | React | Notes for Dioxus | +| --------------------- | ------ | ----- | ------------------------------------------ | +| 1st class router | 👀 | ✅ | Hook built on top of history | +| Assets | 👀 | ✅ | include css/svg/img url statically | +| Integrated classnames | 🛠 | ❓ | built-in `classnames` | +| Suspense | 👀 | 🛠 | schedule future render from future/promise | +| Transition | 👀 | 🛠 | High-level control over suspense | +| Animation | 👀 | ✅ | Spring-style animations | +| Mobile | 👀 | ✅ | Render with cacao | +| Desktop (native) | 👀 | ✅ | Render with native desktop | +| 3D Renderer | 👀 | ✅ | react-three-fiber | + +### Phase 3: Additional Complexity + +| Feature | Dioxus | React | Notes for Dioxus | +| -------------------- | ------ | ----- | ------------------------------------ | +| Portal | ❓ | ✅ | cast elements through tree | +| Error/Panic boundary | ❓ | ✅ | catch panics and display custom BSOD | +| Code-splitting | 👀 | ✅ | Make bundle smaller/lazy | +| LiveView | 👀 | ❓ | Example for SSR + WASM apps | - ✅ = implemented and working - 👀 = not yet implemented or being worked on From 4ecfc241e2d5243f8b1abbe4e339e6b52f903bdf Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 24 Jun 2021 11:18:58 -0400 Subject: [PATCH 16/20] docs: more work --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 28ba8ad84..8bb575380 100644 --- a/README.md +++ b/README.md @@ -92,9 +92,7 @@ And much more. Dioxus makes Rust apps just as fast to write as React apps, but a # Parity with React -Dioxus is heavily inspired by React, but we want your transition to feel like an upgrade. Dioxus is _most_ of the way there, but missing a few key features. This parity table does not necessarily include - -Sorted by priority +Dioxus is heavily inspired by React, but we want your transition to feel like an upgrade. Dioxus is _most_ of the way there, but missing a few key features. This parity table does not necessarily include important ecosystem crates like code blocks, markdown, resizing hooks, etc. ### Phase 1: The Basics From daa9bd82c365763fe240528c7df222d230bce613 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 25 Jun 2021 09:31:13 -0400 Subject: [PATCH 17/20] docs: more work on docs --- .vscode/spellright.dict | 1 + README.md | 8 ++-- examples/rsx_usage.rs | 5 +- notes/Parity.md | 59 ++++++++---------------- packages/core-macro/src/rsx/component.rs | 44 ++++++++++-------- 5 files changed, 53 insertions(+), 64 deletions(-) diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index ee02bfa75..28bfecf22 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -58,3 +58,4 @@ textarea noderefs wasm 7ns +attr diff --git a/README.md b/README.md index 8bb575380..2a6eea9f3 100644 --- a/README.md +++ b/README.md @@ -111,11 +111,13 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an | Null components | ✅ | ✅ | allow returning no components | | No-div components | ✅ | ✅ | components that render components | | Fragments | ✅ | ✅ | rsx! can return multiple elements without a root | -| Manual Props | 👀 | ✅ | Manually pass in props | -| NodeRef | 👀 | ✅ | gain direct access to nodes | +| Manual Props | ✅ | ✅ | Manually pass in props with spread syntax | | Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | -| CSS/Inline Styles | 🛠 | ✅ | syntax for inline/conditional styles | | 1st class global state | 🛠 | ✅ | redux/recoil/mobx on top of context | +| NodeRef | 🛠 | ✅ | gain direct access to nodes [1] | +| CSS/Inline Styles | 🛠 | ✅ | syntax for inline styles/attribute groups | + +[1] Currently blocked until we figure out a cross-platform way of exposing an imperative Node API ### Phase 2: Advanced Toolkits diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index da3d77eba..6fdfb336f 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -162,11 +162,10 @@ static Example: FC<()> = |cx| { // Can take optional properties Taller { a: "asd" } - // Can pass in props directly + // Can pass in props directly as an expression {{ - todo!("this neesd to be implemented"); let props = TallerProps {a: "hello"}; - rsx!(Taller {a: "a"}) + rsx!(Taller { ..props }) }} // Can take children diff --git a/notes/Parity.md b/notes/Parity.md index c838aedbb..76b1cf992 100644 --- a/notes/Parity.md +++ b/notes/Parity.md @@ -1,52 +1,31 @@ # Parity with React -Sorted by priority - -| Feature | Dioxus | React | Notes | -| ---------------------- | ------ | ----- | ------------------------------------------------ | -| ----- Phase 1 ----- | ----- | ----- | ----- | -| Conditional Rendering | ✅ | ✅ | if/then to hide/show component | -| Map, Iterator | ✅ | ✅ | map/filter/reduce rsx! | -| Keyed Components | ✅ | ✅ | advanced diffing with keys | -| Web | ✅ | ✅ | renderer for web browser | -| Desktop (webview) | ✅ | ✅ | renderer for desktop | -| Context | ✅ | ✅ | share state through the tree | -| Hook | ✅ | ✅ | memory cells in components | -| SSR | ✅ | ✅ | render directly to string | -| Runs natively | ✅ | 👀 | runs as a portable binary w/o a runtime (Node) | -| Component Children | ✅ | ✅ | cx.children() as a list of nodes | -| Null components | ✅ | ✅ | allow returning no components | -| No-div components | ✅ | ✅ | components that render components | -| Fragments | ✅ | ✅ | rsx! can return multiple elements without a root | -| Manual Props | 👀 | ✅ | Manually pass in props | -| NodeRef | 👀 | ✅ | gain direct access to nodes | -| Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | -| CSS/Inline Styles | 🛠 | ✅ | syntax for inline/conditional styles | -| 1st class global state | 🛠 | ✅ | redux/recoil/mobx on top of context | -| ----- Phase 2 ----- | ----- | ----- | ----- | -| 1st class router | 👀 | ✅ | Hook built on top of history | -| Assets | 👀 | ✅ | include css/svg/img url statically | -| Integrated classnames | 🛠 | 👀 | built-in `classnames` | -| Suspense | 👀 | 👀 | schedule future render from future/promise | -| Transition | 👀 | 👀 | High-level control over suspense | -| Animation | 👀 | ✅ | Spring-style animations | -| Mobile | 👀 | ✅ | Render with cacao | -| Desktop (native) | 👀 | ✅ | Render with native desktop | -| 3D Renderer | 👀 | ✅ | react-three-fiber | -| ----- Phase 3 ----- | ----- | ----- | ----- | -| Portal | 👀 | ✅ | cast elements through tree | -| Error/Panic boundary | 👀 | ✅ | catch panics and display custom BSOD | -| Code-splitting | 👀 | ✅ | Make bundle smaller/lazy | -| LiveView | 👀 | 👀 | Example for SSR + WASM apps | +Parity has moved to the homepage ## Required services: --- -Gloo is covering a lot of these. We want to build hooks around these, and provide examples on how to use them. +Gloo is covering a lot of these. We want to build hooks around these and provide examples on how to use them. https://github.com/rustwasm/gloo -If the gloo service doesn't exist, then we need to contribute to the project +For example, resize observer would function like this: + +```rust +static Example: FC<()> = |cx| { + let observer = use_resize_observer(); + + cx.render(rsx!( + div { ref: observer.node_ref + "Size, x: {observer.x} y: {observer.y}" + } + )) +} +``` + +However, resize observing is _not_ cross-platform, so this hook (internally) needs to abstract over the rendering platform. + +For other services, we shell out to gloo. If the gloo service doesn't exist, then we need to contribute to the project to make sure it exists. | Service | Hook examples | Current Projects | | ---------------------------- | ------------- | ---------------- | diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs index 3d10aede0..e9b42996b 100644 --- a/packages/core-macro/src/rsx/component.rs +++ b/packages/core-macro/src/rsx/component.rs @@ -27,21 +27,23 @@ pub struct Component { name: syn::Path, body: Vec, children: Vec, + manual_props: Option, } impl Parse for Component { - fn parse(s: ParseStream) -> Result { + fn parse(stream: ParseStream) -> Result { // let name = s.parse::()?; // todo: look into somehow getting the crate/super/etc - let name = syn::Path::parse_mod_style(s)?; + let name = syn::Path::parse_mod_style(stream)?; // parse the guts let content: ParseBuffer; - syn::braced!(content in s); + syn::braced!(content in stream); let mut body: Vec = Vec::new(); let mut children: Vec = Vec::new(); + let mut manual_props = None; 'parsing: loop { // [1] Break if empty @@ -49,15 +51,10 @@ impl Parse for Component { break 'parsing; } - if content.peek(token::Brace) && content.peek2(Token![...]) { - let inner: ParseBuffer; - syn::braced!(inner in content); - if inner.peek(Token![...]) { - todo!("Inline props not yet supported"); - } - } - - if content.peek(Ident) && content.peek2(Token![:]) { + if content.peek(Token![..]) { + content.parse::()?; + manual_props = Some(content.parse::()?); + } else if content.peek(Ident) && content.peek2(Token![:]) { body.push(content.parse::()?); } else { children.push(content.parse::()?); @@ -74,6 +71,7 @@ impl Parse for Component { name, body, children, + manual_props, }) } } @@ -82,8 +80,13 @@ impl ToTokens for Component { fn to_tokens(&self, tokens: &mut TokenStream2) { let name = &self.name; - let mut builder = quote! { - fc_to_builder(#name) + let using_manual_override = self.manual_props.is_some(); + + let mut builder = { + match &self.manual_props { + Some(manual_props) => quote! { #manual_props }, + None => quote! { fc_to_builder(#name) }, + } }; let mut has_key = None; @@ -92,13 +95,18 @@ impl ToTokens for Component { if field.name.to_string() == "key" { has_key = Some(field); } else { - builder.append_all(quote! {#field}); + match using_manual_override { + true => panic!("Currently we don't support manual props and prop fields. Choose either manual props or prop fields"), + false => builder.append_all(quote! {#field}), + } } } - builder.append_all(quote! { - .build() - }); + if !using_manual_override { + builder.append_all(quote! { + .build() + }); + } let key_token = match has_key { Some(field) => { From 57a61fb4bae5f6aacbc82dd0c23f997784891343 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 25 Jun 2021 09:33:43 -0400 Subject: [PATCH 18/20] docs: update readme a tad --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2a6eea9f3..aae6c49b4 100644 --- a/README.md +++ b/README.md @@ -115,9 +115,10 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an | Controlled Inputs | ✅ | ✅ | stateful wrappers around inputs | | 1st class global state | 🛠 | ✅ | redux/recoil/mobx on top of context | | NodeRef | 🛠 | ✅ | gain direct access to nodes [1] | -| CSS/Inline Styles | 🛠 | ✅ | syntax for inline styles/attribute groups | +| CSS/Inline Styles | 🛠 | ✅ | syntax for inline styles/attribute groups[2] | -[1] Currently blocked until we figure out a cross-platform way of exposing an imperative Node API +[1] Currently blocked until we figure out a cross-platform way of exposing an imperative Node API. +[2] Would like to solve this in a more general way. Something like attribute groups that's not styling-specific. ### Phase 2: Advanced Toolkits From 66920eab91ef3d16a9b3b9d6d0ad5dcd4d5aaf8f Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 25 Jun 2021 09:33:59 -0400 Subject: [PATCH 19/20] docs: more readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aae6c49b4..98e2645fe 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,6 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an | LiveView | 👀 | ❓ | Example for SSR + WASM apps | - ✅ = implemented and working -- 👀 = not yet implemented or being worked on - 🛠 = actively being worked on +- 👀 = not yet implemented or being worked on - ❓ = not sure if will or can implement From c8495fd26e2d67edfb7d79e549deae97cbeb2582 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Fri, 25 Jun 2021 09:35:01 -0400 Subject: [PATCH 20/20] docs: more readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 98e2645fe..21fd97d98 100644 --- a/README.md +++ b/README.md @@ -117,8 +117,8 @@ Dioxus is heavily inspired by React, but we want your transition to feel like an | NodeRef | 🛠 | ✅ | gain direct access to nodes [1] | | CSS/Inline Styles | 🛠 | ✅ | syntax for inline styles/attribute groups[2] | -[1] Currently blocked until we figure out a cross-platform way of exposing an imperative Node API. -[2] Would like to solve this in a more general way. Something like attribute groups that's not styling-specific. +- [1] Currently blocked until we figure out a cross-platform way of exposing an imperative Node API. +- [2] Would like to solve this in a more general way. Something like attribute groups that's not styling-specific. ### Phase 2: Advanced Toolkits