diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict index 9205b6f7e..d0a747992 100644 --- a/.vscode/spellright.dict +++ b/.vscode/spellright.dict @@ -25,3 +25,4 @@ Derefing rei RefMut diffed +datafetching diff --git a/packages/cli/src/logging.rs b/packages/cli/src/logging.rs index 2f6b5da5f..94a9ce85b 100644 --- a/packages/cli/src/logging.rs +++ b/packages/cli/src/logging.rs @@ -31,12 +31,14 @@ pub fn set_up_logging() { }) // set the default log level. to filter out verbose log messages from dependencies, set // this to Warn and overwrite the log level for your crate. - .level(log::LevelFilter::Warn) + .level(log::LevelFilter::Info) + // .level(log::LevelFilter::Warn) // change log levels for individual modules. Note: This looks for the record's target // field which defaults to the module path but can be overwritten with the `target` // parameter: // `info!(target="special_target", "This log message is about special_target");` - .level_for("dioxus", log::LevelFilter::Info) + // .level_for("dioxus", log::LevelFilter::Debug) + // .level_for("dioxus", log::LevelFilter::Info) // .level_for("pretty_colored", log::LevelFilter::Trace) // output to stdout .chain(std::io::stdout()) diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 55b45d412..f9634b375 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -31,5 +31,8 @@ id-arena = "2.2.1" thiserror = "1.0.23" fxhash = "0.2.1" longest-increasing-subsequence = "0.1.0" -serde = "1.0.123" +serde = { version = "1.0.123", features = ["derive"] } +log = "0.4.14" +pretty_env_logger = "0.4.0" +ouroboros = "0.8.0" # hashbrown = { version = "0.9.1", features = ["bumpalo"] } diff --git a/packages/core/src/changelist.rs b/packages/core/src/changelist.rs index 382d49422..b9b41c28d 100644 --- a/packages/core/src/changelist.rs +++ b/packages/core/src/changelist.rs @@ -19,6 +19,8 @@ //! //! +use bumpalo::Bump; + use crate::innerlude::{Listener, VirtualDom}; /// The `Edit` represents a single modifcation of the renderer tree. @@ -31,6 +33,7 @@ use crate::innerlude::{Listener, VirtualDom}; /// /// /// +#[derive(Debug)] pub enum Edit<'d> { SetText { text: &'d str }, RemoveSelfAndNextSiblings {}, @@ -57,14 +60,23 @@ pub enum Edit<'d> { } pub struct EditList<'src> { - traversal: Traversal, + pub traversal: Traversal, next_temporary: u32, forcing_new_listeners: bool, - emitter: Vec>, + pub emitter: Vec>, } -/// Traversal methods. -impl EditList<'_> { +impl<'b> EditList<'b> { + pub fn new(bump: &'b Bump) -> Self { + Self { + traversal: Traversal::new(), + next_temporary: 0, + forcing_new_listeners: false, + emitter: Vec::new(), + } + } + + /// Traversal methods. pub fn go_down(&mut self) { self.traversal.down(); } @@ -98,36 +110,37 @@ impl EditList<'_> { pub fn commit_traversal(&mut self) { if self.traversal.is_committed() { + log::debug!("Traversal already committed"); return; } for mv in self.traversal.commit() { match mv { MoveTo::Parent => { - // debug!("emit: pop"); + log::debug!("emit: pop"); self.emitter.push(Edit::Pop {}); // self.emitter.pop(); } MoveTo::Child(n) => { - // debug!("emit: push_child({})", n); + log::debug!("emit: push_child({})", n); self.emitter.push(Edit::PushChild { n }); } MoveTo::ReverseChild(n) => { - // debug!("emit: push_reverse_child({})", n); + log::debug!("emit: push_reverse_child({})", n); self.emitter.push(Edit::PushReverseChild { n }); // self.emitter.push_reverse_child(n); } MoveTo::Sibling(n) => { - // debug!("emit: pop_push_child({})", n); + log::debug!("emit: pop_push_child({})", n); self.emitter.push(Edit::PopPushChild { n }); // self.emitter.pop_push_child(n); } MoveTo::ReverseSibling(n) => { - // debug!("emit: pop_push_reverse_child({})", n); + log::debug!("emit: pop_push_reverse_child({})", n); self.emitter.push(Edit::PopPushReverseChild { n }); } MoveTo::TempChild(temp) => { - // debug!("emit: push_temporary({})", temp); + log::debug!("emit: push_temporary({})", temp); self.emitter.push(Edit::PushTemporary { temp }); // self.emitter.push_temporary(temp); } @@ -477,8 +490,8 @@ impl Traversal { #[inline] pub fn is_committed(&self) -> bool { // is_empty is not inlined? - // self.uncommitted.is_empty() - self.uncommitted.len() == 0 + self.uncommitted.is_empty() + // self.uncommitted.len() == 0 } /// Commit this traversals moves and return the optimized path from the last diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 317c180c4..c5ae7dd9d 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -238,6 +238,8 @@ mod tests { .for_each(|f| assert_eq!(compare_patch(f.0, f.1), true, "{}", description)); } + // todo: make this actually perform real comparisons + // by default, nothing is derived for vnodes or patches fn compare_patch(patch1: &Patch, patch2: &Patch) -> bool { match (patch1, patch2) { (Patch::AppendChildren(_, _), Patch::AppendChildren(_, _)) => true, diff --git a/packages/core/src/dodriodiff.rs b/packages/core/src/dodriodiff.rs index b92158eb1..e6c9fa699 100644 --- a/packages/core/src/dodriodiff.rs +++ b/packages/core/src/dodriodiff.rs @@ -1,3 +1,4 @@ +use bumpalo::Bump; /// Diff the `old` node with the `new` node. Emits instructions to modify a /// physical DOM node that reflects `old` into something that reflects `new`. /// @@ -36,7 +37,7 @@ use fxhash::{FxHashMap, FxHashSet}; use generational_arena::Index; use crate::{ - changelist::EditList, + changelist::{Edit, EditList}, innerlude::{Attribute, Listener, Scope, VElement, VNode, VText}, virtual_dom::LifecycleEvent, }; @@ -55,8 +56,8 @@ use std::cmp::Ordering; /// 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 wereboth updated via /// subscriptions and props changes. -struct DiffingMachine<'a> { - change_list: &'a mut EditList<'a>, +pub struct DiffMachine<'a> { + pub change_list: EditList<'a>, immediate_queue: Vec, diffed: FxHashSet, need_to_diff: FxHashSet, @@ -67,8 +68,21 @@ enum NeedToDiff { Subscription, } -impl<'a> DiffingMachine<'a> { - fn diff_node(&mut self, old: &VNode<'a>, new: &VNode<'a>) { +impl<'a> DiffMachine<'a> { + pub fn new(bump: &'a Bump) -> Self { + Self { + change_list: EditList::new(bump), + immediate_queue: Vec::new(), + diffed: FxHashSet::default(), + need_to_diff: FxHashSet::default(), + } + } + + pub fn consume(self) -> Vec> { + self.change_list.emitter + } + + 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. 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. @@ -107,6 +121,7 @@ impl<'a> DiffingMachine<'a> { // compare elements // if different, schedule different types of update (VNode::Element(eold), VNode::Element(enew)) => { + log::debug!("elements are different"); // 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(); diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 41196957d..bb66209ad 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -65,13 +65,13 @@ //! - dioxus-liveview (SSR + StringRenderer) //! -// pub mod changelist; // An "edit phase" described by transitions and edit operations +pub mod changelist; // An "edit phase" described by transitions and edit operations pub mod component; // Logic for extending FC pub mod context; // Logic for providing hook + context functionality to user components pub mod debug_renderer; // Test harness for validating that lifecycles and diffs work appropriately -pub mod diff; -pub mod patch; // The diffing algorithm that builds the ChangeList - // pub mod dodriodiff; // The diffing algorithm that builds the ChangeList + // pub mod diff; + // pub mod patch; // The diffing algorithm that builds the ChangeList +pub mod dodriodiff; // 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 @@ -99,7 +99,7 @@ pub(crate) mod innerlude { // pub use nodes::iterables::IterableNodes; /// This type alias is an internal way of abstracting over the static functions that represent components. - pub type FC

= for<'a> fn(Context<'a>, &'a P) -> VNode<'a>; + pub type FC

= for<'a> fn(Context<'a>, &'a P) -> &'a VNode<'a>; // pub type FC

= for<'a> fn(Context<'a, P>) -> VNode<'a>; // TODO @Jon, fix this @@ -139,7 +139,8 @@ pub mod prelude { pub use dioxus_core_macro::fc; pub use dioxus_html_2::html; - pub use crate::diff::DiffMachine; + // pub use crate::diff::DiffMachine; + pub use crate::dodriodiff::DiffMachine; pub use crate::hooks::*; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index c721ff93a..43ce30155 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -192,7 +192,7 @@ mod velement { /// Keys must be unique among siblings. /// /// If any sibling is keyed, then they all must be keyed. - #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct NodeKey(pub(crate) u32); impl Default for NodeKey { diff --git a/packages/core/src/scope.rs b/packages/core/src/scope.rs index acdb82d70..fa36f0093 100644 --- a/packages/core/src/scope.rs +++ b/packages/core/src/scope.rs @@ -3,11 +3,82 @@ use crate::innerlude::*; use crate::nodes::VNode; use bumpalo::Bump; use generational_arena::Index; +use owning_ref::StableAddress; use std::{ - any::TypeId, borrow::Borrow, cell::RefCell, future::Future, marker::PhantomData, - sync::atomic::AtomicUsize, todo, + any::TypeId, + borrow::{Borrow, BorrowMut}, + cell::{RefCell, UnsafeCell}, + future::Future, + marker::PhantomData, + ops::{Deref, DerefMut}, + sync::atomic::AtomicUsize, + todo, }; +pub struct BumpContainer(pub UnsafeCell); +impl BumpContainer { + fn new() -> Self { + Self(UnsafeCell::new(Bump::new())) + } +} + +impl Deref for BumpContainer { + type Target = Bump; + + fn deref(&self) -> &Self::Target { + todo!() + // self.0.borrow() + } +} +impl DerefMut for BumpContainer { + fn deref_mut(&mut self) -> &mut Self::Target { + todo!() + // self.0.borrow_mut() + } +} +unsafe impl StableAddress for BumpContainer {} + +#[ouroboros::self_referencing] +pub struct BumpFrame { + pub bump: BumpContainer, + + #[covariant] + #[borrows(bump)] + pub head_node: &'this VNode<'this>, +} + +pub struct ActiveFrame { + pub idx: AtomicUsize, + pub frames: [BumpFrame; 2], +} + +impl ActiveFrame { + fn from_frames(a: BumpFrame, b: BumpFrame) -> Self { + Self { + idx: 0.into(), + frames: [a, b], + } + } + + fn next(&self) -> &BumpFrame { + self.idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + let cur = self.idx.borrow().load(std::sync::atomic::Ordering::Relaxed); + match cur % 1 { + 1 => &self.frames[1], + 0 => &self.frames[0], + _ => unreachable!("mod cannot by non-zero"), + } + } + // fn next(&self) -> &BumpFrame { + // self.idx.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + // let cur = self.idx.borrow().load(std::sync::atomic::Ordering::Relaxed); + // match cur % 2_usize { + // 1 => &self.frames[1], + // 0 => &self.frames[0], + // } + // } +} + /// Every component in Dioxus is represented by a `Scope`. /// /// Scopes contain the state for hooks, the component's props, and other lifecycle information. @@ -26,16 +97,12 @@ pub struct Scope { pub parent: Option, // todo, do better with the active frame stuff - pub frames: [Bump; 2], - // somehow build this vnode with a lifetime tied to self // This root node has "static" lifetime, but it's really not static. // It's goverened by the oldest of the two frames and is switched every time a new render occurs // Use this node as if it were static is unsafe, and needs to be fixed with ourborous or owning ref // ! do not copy this reference are things WILL break ! - pub root_node: *mut VNode<'static>, - - pub active_frame: ActiveFrame, + pub frames: ActiveFrame, // IE Which listeners need to be woken up? pub listeners: Vec>, @@ -45,19 +112,19 @@ pub struct Scope { pub caller: *const i32, } -pub enum ActiveFrame { - First, - Second, -} +// pub enum ActiveFrame { +// First, +// Second, +// } -impl ActiveFrame { - fn next(&mut self) { - match self { - ActiveFrame::First => *self = ActiveFrame::Second, - ActiveFrame::Second => *self = ActiveFrame::First, - } - } -} +// impl ActiveFrame { +// fn next(&mut self) { +// match self { +// ActiveFrame::First => *self = ActiveFrame::Second, +// ActiveFrame::Second => *self = ActiveFrame::First, +// } +// } +// } impl Scope { // create a new scope from a function @@ -72,26 +139,31 @@ impl Scope { // Create the two buffers the componetn will render into // There will always be an "old" and "new" - let frames = [Bump::new(), Bump::new()]; let listeners = Vec::new(); - let active_frame = ActiveFrame::First; + let new_frame = BumpFrameBuilder { + bump: BumpContainer::new(), + head_node_builder: |bump| bump.alloc(VNode::text("")), + } + .build(); - let new = frames[0].alloc(VNode::Text(VText::new(""))); + let old_frame = BumpFrameBuilder { + bump: BumpContainer::new(), + head_node_builder: |bump| bump.alloc(VNode::text("")), + } + .build(); - let cur_node = new as *mut _; + let frames = ActiveFrame::from_frames(old_frame, new_frame); Self { hook_arena, hooks, props_type, caller, - active_frame, + frames, listeners, parent, - frames, - root_node: cur_node, } } @@ -99,37 +171,50 @@ impl Scope { /// This function downcasts the function pointer based on the stored props_type /// /// Props is ?Sized because we borrow the props and don't need to know the size. P (sized) is used as a marker (unsized) - pub(crate) fn run<'a, P: Properties + ?Sized>(&self, props: &'a P) { - let bump = match self.active_frame { - // If the active frame is the first, then we need to bump into the second - ActiveFrame::First => &self.frames[1], - // If the active frame is the second, then we need to bump into the first - ActiveFrame::Second => &self.frames[0], - }; // n.b, there might be a better way of doing this active frame stuff - perhaps swapping + pub(crate) fn run<'a, 'bump, P: Properties + ?Sized>(&'bump mut self, props: &'a P) { + // I really wanted to do this safely, but I don't think we can. + // We want to reset the bump before writing into it. This requires &mut to the bump + // Ouroborous lets us borrow with self, but the heads (IE the source) cannot be changed while the ref is live - let ctx = Context { - scope: &*self, - _p: PhantomData {}, - arena: &self.hook_arena, - hooks: &self.hooks, - idx: 0.into(), - bump, - }; + // n.b, there might be a better way of doing this active frame stuff - perhaps swapping + let frame = self.frames.next(); - /* - SAFETY ALERT + frame.with_bump(|bump_container| { + let bump: &mut Bump = unsafe { &mut *bump_container.0.get() }; + bump.reset(); - This particular usage of transmute is outlined in its docs https://doc.rust-lang.org/std/mem/fn.transmute.html - We hide the generic bound on the function item by casting it to raw pointer. When the function is actually called, - we transmute the function back using the props as reference. + let bump = &*bump; - we could do a better check to make sure that the TypeID is correct before casting - -- - This is safe because we check that the generic type matches before casting. - */ - let caller = unsafe { std::mem::transmute::<*const i32, FC

>(self.caller) }; - let new_nodes = caller(ctx, props); - let old_nodes: &mut VNode<'static> = unsafe { &mut *self.root_node }; + let ctx: Context<'bump> = Context { + scope: &*self, + _p: PhantomData {}, + arena: &self.hook_arena, + hooks: &self.hooks, + idx: 0.into(), + bump, + }; + + /* + SAFETY ALERT + + This particular usage of transmute is outlined in its docs https://doc.rust-lang.org/std/mem/fn.transmute.html + We hide the generic bound on the function item by casting it to raw pointer. When the function is actually called, + we transmute the function back using the props as reference. + + we could do a better check to make sure that the TypeID is correct before casting + -- + This is safe because we check that the generic type matches before casting. + */ + let caller = unsafe { std::mem::transmute::<*const i32, FC

>(self.caller) }; + let nodes: &'bump VNode = caller(ctx, props); + }); + + // let new_nodes = caller(ctx, props); + // let r = new_nodes as *const _; + // self.old_root = self.new_root; + // self.new_root = new_nodes as *const _; + + // let old_nodes: &mut VNode<'static> = unsafe { &mut *self.root_node }; // TODO: Iterate through the new nodes // move any listeners into ourself diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index 14ef55646..78639d61b 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -1,12 +1,14 @@ // use crate::{changelist::EditList, nodes::VNode}; -use crate::nodes::VNode; +use crate::{dodriodiff::DiffMachine, nodes::VNode}; use crate::{events::EventTrigger, innerlude::*}; use any::Any; use bumpalo::Bump; use generational_arena::{Arena, Index}; use std::{ any::{self, TypeId}, + borrow::BorrowMut, cell::{RefCell, UnsafeCell}, + collections::{vec_deque, VecDeque}, future::Future, marker::PhantomData, rc::Rc, @@ -24,7 +26,7 @@ pub struct VirtualDom { /// The index of the root component. base_scope: Index, - event_queue: Rc>>, + event_queue: Rc>>, // Mark the root props with P, even though they're held by the root component // This is done so we don't have a "generic" vdom, making it easier to hold references to it, especially when the holders @@ -65,7 +67,7 @@ impl VirtualDom { let first_event = LifecycleEvent::mount(base_scope, None, 0, root_props); // Create an event queue with a mount for the base scope - let event_queue = Rc::new(RefCell::new(vec![first_event])); + let event_queue = Rc::new(RefCell::new(vec![first_event].into_iter().collect())); let _root_prop_type = TypeId::of::

(); @@ -84,12 +86,15 @@ impl VirtualDom { return Err(Error::WrongProps); } - self.event_queue.borrow_mut().push(LifecycleEvent { - event_type: LifecycleType::PropsChanged { - props: Box::new(new_props), - }, - index: self.base_scope, - }); + self.event_queue + .as_ref() + .borrow_mut() + .push_back(LifecycleEvent { + event_type: LifecycleType::PropsChanged { + props: Box::new(new_props), + }, + component_index: self.base_scope, + }); Ok(()) } @@ -103,8 +108,13 @@ impl VirtualDom { /// Update the root props, and progress /// Takes a bump arena to allocate into, making the diff phase as fast as possible pub fn progress(&mut self) -> Result<()> { - let event = self.event_queue.borrow_mut().pop().ok_or(Error::NoEvent)?; - process_event(&mut self.components, event) + let event = self + .event_queue + .as_ref() + .borrow_mut() + .pop_front() + .ok_or(Error::NoEvent)?; + self.process_event(event) } /// This method is the most sophisticated way of updating the virtual dom after an external event has been triggered. @@ -151,85 +161,93 @@ impl VirtualDom { // Prop updates take prescedence over subscription updates // Run all prop updates *first* as they will cascade into children. // *then* run the non-prop updates that were not already covered by props - let mut events = self.event_queue.borrow_mut(); - // for now, just naively process each event in the queue - for event in events.drain(..) { - process_event(&mut self.components, event)?; + let mut affected_components = Vec::new(); + // It's essentially draining the vec, but with some dancing to release the RefMut + // We also want to be able to push events into the queue from processing the event + while let Some(event) = { + let new_evt = self.event_queue.as_ref().borrow_mut().pop_front(); + new_evt + } { + affected_components.push(event.component_index); + self.process_event(event)?; } - // todo!() + let diff_bump = Bump::new(); + let diff_machine = DiffMachine::new(&diff_bump); + Ok(()) } pub async fn progress_completely(&mut self) -> Result<()> { Ok(()) } -} + /// Using mutable access to the Virtual Dom, progress a given lifecycle event + /// + /// + /// + /// + /// + /// + fn process_event( + &mut self, + LifecycleEvent { + component_index: index, + event_type, + }: LifecycleEvent, + ) -> Result<()> { + let scope = self.components.get_mut(index).ok_or(Error::NoEvent)?; -/// Using mutable access to the Virtual Dom, progress a given lifecycle event -/// -/// -/// -/// -/// -/// -fn process_event( - // dom: &mut VirtualDom

, - components: &mut Arena, - LifecycleEvent { index, event_type }: LifecycleEvent, -) -> Result<()> { - let scope = components.get(index).ok_or(Error::NoEvent)?; + match event_type { + // Component needs to be mounted to the virtual dom + LifecycleType::Mount { to, under, props } => { + if let Some(other) = to { + // mount to another component + } else { + // mount to the root + } - match event_type { - // Component needs to be mounted to the virtual dom - LifecycleType::Mount { to, under, props } => { - if let Some(other) = to { - // mount to another component - } else { - // mount to the root + let g = props.as_ref(); + scope.run(g); + // scope.run(runner, props, dom); } - let g = props.as_ref(); - scope.run(g); - // scope.run(runner, props, dom); - } + // The parent for this component generated new props and the component needs update + LifecycleType::PropsChanged { props } => { + // + } - // The parent for this component generated new props and the component needs update - LifecycleType::PropsChanged { props } => { + // Component was successfully mounted to the dom + LifecycleType::Mounted {} => { + // + } + + // Component was removed from the DOM + // Run any destructors and cleanup for the hooks and the dump the component + LifecycleType::Removed {} => { + let f = self.components.remove(index); + // let f = dom.components.remove(index); + } + + // Component was messaged via the internal subscription service + LifecycleType::Messaged => { + // + } + + // Event from renderer was fired with a given listener ID // + LifecycleType::Callback { listener_id } => {} + + // Run any post-render callbacks on a component + LifecycleType::Rendered => {} } - // Component was successfully mounted to the dom - LifecycleType::Mounted {} => { - // - } - - // Component was removed from the DOM - // Run any destructors and cleanup for the hooks and the dump the component - LifecycleType::Removed {} => { - let f = components.remove(index); - // let f = dom.components.remove(index); - } - - // Component was messaged via the internal subscription service - LifecycleType::Messaged => { - // - } - - // Event from renderer was fired with a given listener ID - // - LifecycleType::Callback { listener_id } => {} - - // Run any post-render callbacks on a component - LifecycleType::Rendered => {} + Ok(()) } - - Ok(()) } pub struct LifecycleEvent { - pub index: Index, + pub component_index: Index, pub event_type: LifecycleType, } @@ -265,7 +283,7 @@ impl LifecycleEvent { props: P, ) -> Self { Self { - index: which, + component_index: which, event_type: LifecycleType::Mount { to, under, diff --git a/packages/web/Cargo.toml b/packages/web/Cargo.toml index 829874c17..309250809 100644 --- a/packages/web/Cargo.toml +++ b/packages/web/Cargo.toml @@ -17,6 +17,8 @@ futures = "0.3.12" wasm-logger = "0.2.0" log = "0.4.14" fxhash = "0.2.1" +pretty_env_logger = "0.4.0" +console_error_panic_hook = "0.1.6" # html-validation = { path = "../html-validation", version = "0.1.1" } [dependencies.web-sys] @@ -36,6 +38,7 @@ features = [ "MouseEvent", "InputEvent", "DocumentType", + "CharacterData", ] [profile.release] diff --git a/packages/web/examples/basic.rs b/packages/web/examples/basic.rs index 7cb92c868..5cfc4aebc 100644 --- a/packages/web/examples/basic.rs +++ b/packages/web/examples/basic.rs @@ -8,42 +8,47 @@ use dioxus_web::*; fn main() { wasm_logger::init(wasm_logger::Config::new(log::Level::Debug)); - // log::debug!("Hello world, from the app"); + console_error_panic_hook::set_once(); WebsysRenderer::simple_render(html! { - - // Body -

-
-
- // Title -
- // {format!("Fibonacci Calculator: n = {}",n)} - "Jon's awesome site!!11" -
- - // Subtext / description -
- // {format!("Calculated in {} nanoseconds",duration)} - // {format!("Calculated in {} nanoseconds",duration)} - "He worked so hard on it :)" -
- -
- // Main number -
- "1337" -
-
- - // Try another - -
+
+
+
"Count is {}"
+ +
- }); + // WebsysRenderer::simple_render(html! { + //
+ //
+ //
+ //
+ // // Title + //
+ // "Jon's awesome site!!11" + //
+ + // // Subtext / description + //
+ // "He worked so hard on it :)" + //
+ + //
+ // // Main number + //
+ // "1337" + //
+ //
+ + // // Try another + // + //
+ //
+ //
+ //
+ // }); } diff --git a/packages/web/src/interpreter.rs b/packages/web/src/interpreter.rs index ba9473240..b7bc8ae5d 100644 --- a/packages/web/src/interpreter.rs +++ b/packages/web/src/interpreter.rs @@ -1,17 +1,16 @@ -// use crate::cached_set::CacheId; -// use crate::{Element, EventsTrampoline}; +use dioxus_core::changelist::Edit; use fxhash::FxHashMap; use log::{debug, info, log}; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{window, Document, Element, Event, Node}; #[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)] -pub(crate) struct CacheId(u32); +pub struct CacheId(u32); #[derive(Debug)] -pub(crate) struct ChangeListInterpreter { +pub struct PatchMachine { container: Element, - stack: Stack, + pub stack: Stack, temporaries: FxHashMap, templates: FxHashMap, callback: Option>, @@ -19,7 +18,7 @@ pub(crate) struct ChangeListInterpreter { } #[derive(Debug, Default)] -struct Stack { +pub struct Stack { list: Vec, } @@ -47,11 +46,19 @@ impl Stack { } pub fn top(&self) -> &Node { - &self.list[self.list.len() - 1] + log::info!( + "Called top of stack with {} items remaining", + self.list.len() + ); + match self.list.last() { + Some(a) => a, + None => panic!("should not happen"), + } + // &self.list[self.list.len() - 1] } } -impl ChangeListInterpreter { +impl PatchMachine { pub fn new(container: Element) -> Self { let document = window() .expect("must have access to the window") @@ -114,6 +121,265 @@ impl ChangeListInterpreter { // }) as Box)); } + pub fn handle_edit(&mut self, edit: &Edit) { + match *edit { + // 0 + Edit::SetText { text } => { + // + self.stack.top().set_text_content(Some(text)) + } + + // 1 + Edit::RemoveSelfAndNextSiblings {} => { + let node = self.stack.pop(); + let mut sibling = node.next_sibling(); + + while let Some(inner) = sibling { + let temp = inner.next_sibling(); + if let Some(sibling) = inner.dyn_ref::() { + sibling.remove(); + } + sibling = temp; + } + if let Some(node) = node.dyn_ref::() { + node.remove(); + } + } + + // 2 + Edit::ReplaceWith => { + 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); + } + + self.stack.push(new_node); + } + + // 3 + Edit::SetAttribute { name, value } => { + let node = self.stack.top(); + + if let Some(node) = node.dyn_ref::() { + node.set_attribute(name, value).unwrap(); + + // Some attributes are "volatile" and don't work through `setAttribute`. + // TODO: + // if name == "value" { + // node.set_value(value); + // } + // if name == "checked" { + // node.set_checked(true); + // } + // if name == "selected" { + // node.set_selected(true); + // } + } + } + + // 4 + Edit::RemoveAttribute { name } => { + let node = self.stack.top(); + if let Some(node) = node.dyn_ref::() { + node.remove_attribute(name).unwrap(); + + // Some attributes are "volatile" and don't work through `removeAttribute`. + // TODO: + // if name == "value" { + // node.set_value(""); + // } + // if name == "checked" { + // node.set_checked(false); + // } + // if name == "selected" { + // node.set_selected(false); + // } + } + } + + // 5 + Edit::PushReverseChild { n } => { + let parent = self.stack.top(); + let children = parent.child_nodes(); + let child = children.get(children.length() - n - 1).unwrap(); + self.stack.push(child); + } + + // 6 + Edit::PopPushChild { n } => { + self.stack.pop(); + let parent = self.stack.top(); + let children = parent.child_nodes(); + let child = children.get(n).unwrap(); + self.stack.push(child); + } + + // 7 + Edit::Pop => { + self.stack.pop(); + } + + // 8 + Edit::AppendChild => { + let child = self.stack.pop(); + self.stack.top().append_child(&child).unwrap(); + } + + // 9 + Edit::CreateTextNode { text } => self.stack.push( + self.document + .create_text_node(text) + .dyn_into::() + .unwrap(), + ), + + // 10 + Edit::CreateElement { tag_name } => { + let el = self + .document + .create_element(tag_name) + .unwrap() + .dyn_into::() + .unwrap(); + self.stack.push(el); + } + + // 11 + Edit::NewEventListener { event_type, a, b } => { + let el = self.stack.top(); + + let el = el + .dyn_ref::() + .expect(&format!("not an element: {:?}", el)); + el.add_event_listener_with_callback( + event_type, + self.callback.as_ref().unwrap().as_ref().unchecked_ref(), + ) + .unwrap(); + debug!("adding attributes: {}, {}", a, b); + el.set_attribute(&format!("dodrio-a-{}", event_type), &a.to_string()) + .unwrap(); + el.set_attribute(&format!("dodrio-b-{}", event_type), &b.to_string()) + .unwrap(); + } + + // 12 + Edit::UpdateEventListener { event_type, a, b } => { + if let Some(el) = self.stack.top().dyn_ref::() { + el.set_attribute(&format!("dodrio-a-{}", event_type), &a.to_string()) + .unwrap(); + el.set_attribute(&format!("dodrio-b-{}", event_type), &b.to_string()) + .unwrap(); + } + } + + // 13 + Edit::RemoveEventListener { event_type } => { + if let Some(el) = self.stack.top().dyn_ref::() { + el.remove_event_listener_with_callback( + event_type, + self.callback.as_ref().unwrap().as_ref().unchecked_ref(), + ) + .unwrap(); + } + } + + // 14 + Edit::CreateElementNs { tag_name, ns } => { + let el = self + .document + .create_element_ns(Some(ns), tag_name) + .unwrap() + .dyn_into::() + .unwrap(); + self.stack.push(el); + } + + // 15 + Edit::SaveChildrenToTemporaries { + mut temp, + start, + end, + } => { + let parent = self.stack.top(); + let children = parent.child_nodes(); + for i in start..end { + self.temporaries.insert(temp, children.get(i).unwrap()); + temp += 1; + } + } + + // 16 + Edit::PushChild { n } => { + let parent = self.stack.top(); + let child = parent.child_nodes().get(n).unwrap(); + self.stack.push(child); + } + + // 17 + Edit::PushTemporary { temp } => { + let t = self.temporaries.get(&temp).unwrap().clone(); + self.stack.push(t); + } + + // 18 + Edit::InsertBefore => { + let before = self.stack.pop(); + let after = self.stack.pop(); + after + .parent_node() + .unwrap() + .insert_before(&before, Some(&after)) + .unwrap(); + self.stack.push(before); + } + + // 19 + Edit::PopPushReverseChild { n } => { + self.stack.pop(); + let parent = self.stack.top(); + let children = parent.child_nodes(); + let child = children.get(children.length() - n - 1).unwrap(); + self.stack.push(child); + } + + // 20 + Edit::RemoveChild { n } => { + let parent = self.stack.top(); + if let Some(child) = parent.child_nodes().get(n).unwrap().dyn_ref::() { + child.remove(); + } + } + + // 21 + Edit::SetClass { class_name } => { + if let Some(el) = self.stack.top().dyn_ref::() { + el.set_class_name(class_name); + } + } + } + } + // 0 pub fn set_text(&mut self, text: &str) { self.stack.top().set_text_content(Some(text)); diff --git a/packages/web/src/lib.rs b/packages/web/src/lib.rs index fcc0a6fda..9da398350 100644 --- a/packages/web/src/lib.rs +++ b/packages/web/src/lib.rs @@ -1,5 +1,5 @@ //! Dioxus WebSys -//! +//! -------------- //! This crate implements a renderer of the Dioxus Virtual DOM for the web browser. //! //! While it is possible to render a single component directly, it is not possible to render component trees. For these, @@ -16,8 +16,11 @@ //! ``` //! //! The `WebsysRenderer` is particularly useful when needing to cache a Virtual DOM in between requests +use web_sys::{window, Document, Element, Event, Node}; -use dioxus::{patch::Patch, prelude::VText}; +use dioxus::prelude::VElement; +// use dioxus::{patch::Patch, prelude::VText}; +// use dioxus::{patch::Patch, prelude::VText}; pub use dioxus_core as dioxus; use dioxus_core::{ events::EventTrigger, @@ -26,6 +29,7 @@ use dioxus_core::{ use futures::{channel::mpsc, future, SinkExt, StreamExt}; use mpsc::UnboundedSender; pub mod interpreter; +use interpreter::PatchMachine; /// 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 @@ -71,18 +75,18 @@ impl WebsysRenderer { let (sender, mut receiver) = mpsc::unbounded::(); // Iterate through the nodes, attaching the closure and sender to the listener - { - let mut remote_sender = sender.clone(); - let listener = move || { - let event = EventTrigger::new(); - wasm_bindgen_futures::spawn_local(async move { - remote_sender - .send(event) - .await - .expect("Updating receiver failed"); - }) - }; - } + // { + // let mut remote_sender = sender.clone(); + // let listener = move || { + // let event = EventTrigger::new(); + // wasm_bindgen_futures::spawn_local(async move { + // remote_sender + // .send(event) + // .await + // .expect("Updating receiver failed"); + // }) + // }; + // } // Event loop waits for the receiver to finish up // TODO! Connect the sender to the virtual dom's suspense system @@ -105,433 +109,116 @@ impl WebsysRenderer { pub fn simple_render(tree: impl for<'a> Fn(&'a Bump) -> VNode<'a>) { let bump = Bump::new(); + // Choose the body to render the app into + let window = web_sys::window().expect("should have access to the Window"); + let document = window + .document() + .expect("should have access to the Document"); + let body = document.body().unwrap(); + + // Build a dummy div + let container: &Element = body.as_ref(); + container.set_inner_html(""); + container + .append_child( + document + .create_element("div") + .expect("should create element OK") + .as_ref(), + ) + .expect("should append child OK"); + + // Create the old dom and the new dom + // The old is just an empty div, like the one we made above let old = html! {
}(&bump); - - let created = create_dom_node(&old); - let root_node = created.node; - let new = tree(&bump); - let mut machine = DiffMachine::new(); + // Build a machine that diffs doms + let mut diff_machine = DiffMachine::new(&bump); + diff_machine.diff_node(&old, &new); - let patches = machine.diff(&old, &new); - // log::info!("There are {:?} patches", patches.len()); + // Build a machine that patches doms + // In practice, the diff machine might be on a different computer, sending us patches + let mut patch_machine = PatchMachine::new(body.clone().into()); - let root2 = root_node.clone(); - patch(root_node, &patches).expect("Failed to simple render"); - let document = web_sys::window().unwrap().document().unwrap(); + // need to make sure we push the root node onto the stack before trying to run anything + // this provides an entrance for the diffing machine to do its work + // Here, we grab the div out of the container (the body) to connect with the dummy div we made above + // This is because we don't support fragments (yet) + let root_node = container.first_child().unwrap(); + patch_machine.stack.push(root_node); - document.body().unwrap().append_child(&root2); - // log::info!("Succesfully patched the dom"); - } -} - -use std::collections::HashMap; -use std::collections::HashSet; -use std::{cmp::min, rc::Rc}; -use wasm_bindgen::JsCast; -use wasm_bindgen::JsValue; -use web_sys::{Element, Node, Text}; - -/// Apply all of the patches to our old root node in order to create the new root node -/// that we desire. -/// This is usually used after diffing two virtual nodes. -pub fn patch>(root_node: N, patches: &Vec) -> Result<(), JsValue> { - // pub fn patch>(root_node: N, patches: &Vec) -> Result { - let root_node: Node = root_node.into(); - - let mut cur_node_idx = 0; - - let mut nodes_to_find = HashSet::new(); - - for patch in patches { - nodes_to_find.insert(patch.node_idx()); - } - - let mut element_nodes_to_patch = HashMap::new(); - let mut text_nodes_to_patch = HashMap::new(); - - // Closures that were added to the DOM during this patch operation. - // let mut active_closures = HashMap::new(); - - find_nodes( - root_node, - &mut cur_node_idx, - &mut nodes_to_find, - &mut element_nodes_to_patch, - &mut text_nodes_to_patch, - ); - - for patch in patches { - let patch_node_idx = patch.node_idx(); - - if let Some(element) = element_nodes_to_patch.get(&patch_node_idx) { - let new_closures = apply_element_patch(&element, &patch)?; - // active_closures.extend(new_closures); - continue; + // Consume the diff machine, generating the patch list + for patch in diff_machine.consume() { + patch_machine.handle_edit(&patch); + log::info!("Patch is {:?}", patch); } - - if let Some(text_node) = text_nodes_to_patch.get(&patch_node_idx) { - apply_text_patch(&text_node, &patch)?; - continue; - } - - unreachable!("Getting here means we didn't find the element or next node that we were supposed to patch.") } - // Ok(active_closures) - Ok(()) -} + pub fn complex_render( + tree1: impl for<'a> Fn(&'a Bump) -> VNode<'a>, + tree2: impl for<'a> Fn(&'a Bump) -> VNode<'a>, + ) { + let bump = Bump::new(); -fn find_nodes( - root_node: Node, - cur_node_idx: &mut usize, - nodes_to_find: &mut HashSet, - element_nodes_to_patch: &mut HashMap, - text_nodes_to_patch: &mut HashMap, -) { - if nodes_to_find.len() == 0 { - return; - } + let old = tree1(&bump); + let new = tree2(&bump); - // We use child_nodes() instead of children() because children() ignores text nodes - let children = root_node.child_nodes(); - let child_node_count = children.length(); + let mut machine = DiffMachine::new(&bump); + machine.diff_node(&old, &new); - // If the root node matches, mark it for patching - if nodes_to_find.get(&cur_node_idx).is_some() { - match root_node.node_type() { - Node::ELEMENT_NODE => { - element_nodes_to_patch.insert(*cur_node_idx, root_node.unchecked_into()); - } - Node::TEXT_NODE => { - text_nodes_to_patch.insert(*cur_node_idx, root_node.unchecked_into()); - } - other => unimplemented!("Unsupported root node type: {}", other), - } - nodes_to_find.remove(&cur_node_idx); - } - - *cur_node_idx += 1; - - for i in 0..child_node_count { - let node = children.item(i).unwrap(); - - match node.node_type() { - Node::ELEMENT_NODE => { - find_nodes( - node, - cur_node_idx, - nodes_to_find, - element_nodes_to_patch, - text_nodes_to_patch, - ); - } - Node::TEXT_NODE => { - if nodes_to_find.get(&cur_node_idx).is_some() { - text_nodes_to_patch.insert(*cur_node_idx, node.unchecked_into()); - } - - *cur_node_idx += 1; - } - Node::COMMENT_NODE => { - // At this time we do not support user entered comment nodes, so if we see a comment - // then it was a delimiter created by virtual-dom-rs in order to ensure that two - // neighboring text nodes did not get merged into one by the browser. So we skip - // over this virtual-dom-rs generated comment node. - } - _other => { - // Ignoring unsupported child node type - // TODO: What do we do with this situation? Log a warning? - } + for patch in machine.consume() { + println!("Patch is {:?}", patch); } } } -// pub type ActiveClosures = HashMap>; +#[cfg(test)] +mod tests { + use std::env; -// fn apply_element_patch(node: &Element, patch: &Patch) -> Result { -fn apply_element_patch(node: &Element, patch: &Patch) -> Result<(), JsValue> { - // let active_closures = HashMap::new(); + use super::*; + use dioxus_core as dioxus; + use dioxus_core::prelude::html; - match patch { - Patch::AddAttributes(_node_idx, attributes) => { - for (attrib_name, attrib_val) in attributes.iter() { - node.set_attribute(attrib_name, attrib_val)?; - } - - // Ok(active_closures) - Ok(()) - } - Patch::RemoveAttributes(_node_idx, attributes) => { - for attrib_name in attributes.iter() { - node.remove_attribute(attrib_name)?; - } - - // Ok(active_closures) - Ok(()) - } - Patch::Replace(_node_idx, new_node) => { - let created_node = create_dom_node(&new_node); - - node.replace_with_with_node_1(&created_node.node)?; - - Ok(()) - // Ok(created_node.closures) - } - Patch::TruncateChildren(_node_idx, num_children_remaining) => { - let children = node.child_nodes(); - let mut child_count = children.length(); - - // We skip over any separators that we placed between two text nodes - // -> `` - // and trim all children that come after our new desired `num_children_remaining` - let mut non_separator_children_found = 0; - - for index in 0 as u32..child_count { - let child = children - .get(min(index, child_count - 1)) - .expect("Potential child to truncate"); - - // If this is a comment node then we know that it is a `` - // text node separator that was created in virtual_node/mod.rs. - if child.node_type() == Node::COMMENT_NODE { - continue; - } - - non_separator_children_found += 1; - - if non_separator_children_found <= *num_children_remaining as u32 { - continue; - } - - node.remove_child(&child).expect("Truncated children"); - child_count -= 1; - } - - Ok(()) - // Ok(active_closures) - } - Patch::AppendChildren(_node_idx, new_nodes) => { - let parent = &node; - - let mut active_closures = HashMap::new(); - - for new_node in new_nodes { - let created_node = create_dom_node(&new_node); - // let created_node = new_node.create_dom_node(); - - parent.append_child(&created_node.node)?; - - active_closures.extend(created_node.closures); - } - - Ok(()) - // Ok(active_closures) - } - Patch::ChangeText(_node_idx, _new_node) => { - unreachable!("Elements should not receive ChangeText patches.") - } - } -} - -fn apply_text_patch(node: &Text, patch: &Patch) -> Result<(), JsValue> { - match patch { - Patch::ChangeText(_node_idx, new_node) => { - node.set_node_value(Some(&new_node.text)); - } - Patch::Replace(_node_idx, new_node) => { - node.replace_with_with_node_1(&create_dom_node(&new_node).node)?; - // node.replace_with_with_node_1(&new_node.create_dom_node().node)?; - } - other => unreachable!( - "Text nodes should only receive ChangeText or Replace patches, not ", - // other, - // "Text nodes should only receive ChangeText or Replace patches, not {:?}.", - // other, - ), - }; - - Ok(()) -} - -/// A node along with all of the closures that were created for that -/// node's events and all of it's child node's events. -pub struct CreatedNode { - /// A `Node` or `Element` that was created from a `VirtualNode` - pub node: T, - /// A map of a node's unique identifier along with all of the Closures for that node. - /// - /// The DomUpdater uses this to look up nodes and see if they're still in the page. If not - /// the reference that we maintain to their closure will be dropped, thus freeing the Closure's - /// memory. - pub closures: HashMap>, -} - -/// Box>> is our js_sys::Closure. Stored this way to allow us to store -/// any Closure regardless of the arguments. -pub type DynClosure = Rc>; - -impl CreatedNode { - pub fn without_closures>(node: N) -> Self { - CreatedNode { - node: node.into(), - closures: HashMap::with_capacity(0), - } - } -} - -impl std::ops::Deref for CreatedNode { - type Target = T; - fn deref(&self) -> &Self::Target { - &self.node - } -} - -impl From> for CreatedNode { - fn from(other: CreatedNode) -> CreatedNode { - CreatedNode { - node: other.node.into(), - closures: other.closures, - } - } -} -fn create_dom_node(node: &VNode<'_>) -> CreatedNode { - match node { - VNode::Text(text_node) => CreatedNode::without_closures(create_text_node(text_node)), - VNode::Element(element_node) => create_element_node(element_node).into(), - // VNode::Element(element_node) => element_node.create_element_node().into(), - VNode::Suspended => todo!(" not iimplemented yet"), - VNode::Component(_) => todo!(" not iimplemented yet"), - } -} - -/// Build a DOM element by recursively creating DOM nodes for this element and it's -/// children, it's children's children, etc. -pub fn create_element_node(node: &dioxus_core::nodes::VElement) -> CreatedNode { - let document = web_sys::window().unwrap().document().unwrap(); - - // TODO: enable svg again - // let element = if html_validation::is_svg_namespace(&node.tag_name) { - // document - // .create_element_ns(Some("http://www.w3.org/2000/svg"), &node.tag_name) - // .unwrap() - // } else { - let element = document.create_element(&node.tag_name).unwrap(); - // }; - - let mut closures = HashMap::new(); - - node.attributes - .iter() - .map(|f| (f.name, f.value)) - .for_each(|(name, value)| { - if name == "unsafe_inner_html" { - element.set_inner_html(value); - - return; - } - - element - .set_attribute(name, value) - .expect("Set element attribute in create element"); + #[test] + fn simple_patch() { + env::set_var("RUST_LOG", "trace"); + pretty_env_logger::init(); + log::info!("Hello!"); + let renderer = WebsysRenderer::simple_render(html! { +
+ "Hello world" + + +
}); + } - // if node.events.0.len() > 0 { - // let unique_id = create_unique_identifier(); - - // element - // .set_attribute("data-vdom-id".into(), &unique_id.to_string()) - // .expect("Could not set attribute on element"); - - // closures.insert(unique_id, vec![]); - - // node.events.0.iter().for_each(|(onevent, callback)| { - // // onclick -> click - // let event = &onevent[2..]; - - // let current_elem: &EventTarget = element.dyn_ref().unwrap(); - - // current_elem - // .add_event_listener_with_callback(event, callback.as_ref().as_ref().unchecked_ref()) - // .unwrap(); - - // closures - // .get_mut(&unique_id) - // .unwrap() - // .push(Rc::clone(callback)); - // }); - // } - - let mut previous_node_was_text = false; - - node.children.iter().for_each(|child| { - // log::info!("Patching child"); - match child { - VNode::Text(text_node) => { - let current_node = element.as_ref() as &web_sys::Node; - - // 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 - if previous_node_was_text { - let separator = document.create_comment("ptns"); - current_node - .append_child(separator.as_ref() as &web_sys::Node) - .unwrap(); - } - - current_node - .append_child(&create_text_node(&text_node)) - .unwrap(); - - previous_node_was_text = true; - } - VNode::Element(element_node) => { - previous_node_was_text = false; - - let child = create_element_node(element_node); - // let child = element_node.create_element_node(); - let child_elem: Element = child.node; - - closures.extend(child.closures); - - element.append_child(&child_elem).unwrap(); - } - VNode::Suspended => { - todo!("Not yet supported") - } - VNode::Component(_) => { - todo!("Not yet supported") - } - } - }); - - // TODO: connect on mount to the event system somehow - // if let Some(on_create_elem) = node.events.0.get("on_create_elem") { - // let on_create_elem: &js_sys::Function = on_create_elem.as_ref().as_ref().unchecked_ref(); - // on_create_elem - // .call1(&wasm_bindgen::JsValue::NULL, &element) - // .unwrap(); - // } - - CreatedNode { - node: element, - closures, + #[test] + fn complex_patch() { + env::set_var("RUST_LOG", "trace"); + pretty_env_logger::init(); + log::info!("Hello!"); + let renderer = WebsysRenderer::complex_render( + html! { +
+ "Hello world" +
+

"Heading"

+
+
+ }, + html! { +
+ "Hello world" + "Hello world" + "Hello world" +
+

"Heading"

+
+
+ }, + ); } } - -/// Return a `Text` element from a `VirtualNode`, typically right before adding it -/// into the DOM. -pub fn create_text_node(node: &VText) -> Text { - let document = web_sys::window().unwrap().document().unwrap(); - document.create_text_node(&node.text) -} - -// /// For any listeners in the tree, attach the sender closure. -// /// When a event is triggered, we convert it into the synthetic event type and dump it back in the Virtual Dom's queu -// fn attach_listeners(sender: &UnboundedSender, dom: &VirtualDom) {} -// fn render_diffs() {} diff --git a/packages/web/src/percypatch.rs b/packages/web/src/percypatch.rs new file mode 100644 index 000000000..9f7b1ae6d --- /dev/null +++ b/packages/web/src/percypatch.rs @@ -0,0 +1,409 @@ +use std::collections::HashMap; +use std::collections::HashSet; +use std::{cmp::min, rc::Rc}; +use wasm_bindgen::JsCast; +use wasm_bindgen::JsValue; +use web_sys::{Element, Node, Text}; + +/// Apply all of the patches to our old root node in order to create the new root node +/// that we desire. +/// This is usually used after diffing two virtual nodes. +pub fn patch>(root_node: N, patches: &Vec) -> Result<(), JsValue> { + // pub fn patch>(root_node: N, patches: &Vec) -> Result { + let root_node: Node = root_node.into(); + + let mut cur_node_idx = 0; + + let mut nodes_to_find = HashSet::new(); + + for patch in patches { + nodes_to_find.insert(patch.node_idx()); + } + + let mut element_nodes_to_patch = HashMap::new(); + let mut text_nodes_to_patch = HashMap::new(); + + // Closures that were added to the DOM during this patch operation. + // let mut active_closures = HashMap::new(); + + find_nodes( + root_node, + &mut cur_node_idx, + &mut nodes_to_find, + &mut element_nodes_to_patch, + &mut text_nodes_to_patch, + ); + + for patch in patches { + let patch_node_idx = patch.node_idx(); + + if let Some(element) = element_nodes_to_patch.get(&patch_node_idx) { + let new_closures = apply_element_patch(&element, &patch)?; + // active_closures.extend(new_closures); + continue; + } + + if let Some(text_node) = text_nodes_to_patch.get(&patch_node_idx) { + apply_text_patch(&text_node, &patch)?; + continue; + } + + unreachable!("Getting here means we didn't find the element or next node that we were supposed to patch.") + } + + // Ok(active_closures) + Ok(()) +} + +fn find_nodes( + root_node: Node, + cur_node_idx: &mut usize, + nodes_to_find: &mut HashSet, + element_nodes_to_patch: &mut HashMap, + text_nodes_to_patch: &mut HashMap, +) { + if nodes_to_find.len() == 0 { + return; + } + + // We use child_nodes() instead of children() because children() ignores text nodes + let children = root_node.child_nodes(); + let child_node_count = children.length(); + + // If the root node matches, mark it for patching + if nodes_to_find.get(&cur_node_idx).is_some() { + match root_node.node_type() { + Node::ELEMENT_NODE => { + element_nodes_to_patch.insert(*cur_node_idx, root_node.unchecked_into()); + } + Node::TEXT_NODE => { + text_nodes_to_patch.insert(*cur_node_idx, root_node.unchecked_into()); + } + other => unimplemented!("Unsupported root node type: {}", other), + } + nodes_to_find.remove(&cur_node_idx); + } + + *cur_node_idx += 1; + + for i in 0..child_node_count { + let node = children.item(i).unwrap(); + + match node.node_type() { + Node::ELEMENT_NODE => { + find_nodes( + node, + cur_node_idx, + nodes_to_find, + element_nodes_to_patch, + text_nodes_to_patch, + ); + } + Node::TEXT_NODE => { + if nodes_to_find.get(&cur_node_idx).is_some() { + text_nodes_to_patch.insert(*cur_node_idx, node.unchecked_into()); + } + + *cur_node_idx += 1; + } + Node::COMMENT_NODE => { + // At this time we do not support user entered comment nodes, so if we see a comment + // then it was a delimiter created by virtual-dom-rs in order to ensure that two + // neighboring text nodes did not get merged into one by the browser. So we skip + // over this virtual-dom-rs generated comment node. + } + _other => { + // Ignoring unsupported child node type + // TODO: What do we do with this situation? Log a warning? + } + } + } +} + +// pub type ActiveClosures = HashMap>; + +// fn apply_element_patch(node: &Element, patch: &Patch) -> Result { +fn apply_element_patch(node: &Element, patch: &Patch) -> Result<(), JsValue> { + // let active_closures = HashMap::new(); + + match patch { + Patch::AddAttributes(_node_idx, attributes) => { + for (attrib_name, attrib_val) in attributes.iter() { + node.set_attribute(attrib_name, attrib_val)?; + } + + // Ok(active_closures) + Ok(()) + } + Patch::RemoveAttributes(_node_idx, attributes) => { + for attrib_name in attributes.iter() { + node.remove_attribute(attrib_name)?; + } + + // Ok(active_closures) + Ok(()) + } + Patch::Replace(_node_idx, new_node) => { + let created_node = create_dom_node(&new_node); + + node.replace_with_with_node_1(&created_node.node)?; + + Ok(()) + // Ok(created_node.closures) + } + Patch::TruncateChildren(_node_idx, num_children_remaining) => { + let children = node.child_nodes(); + let mut child_count = children.length(); + + // We skip over any separators that we placed between two text nodes + // -> `` + // and trim all children that come after our new desired `num_children_remaining` + let mut non_separator_children_found = 0; + + for index in 0 as u32..child_count { + let child = children + .get(min(index, child_count - 1)) + .expect("Potential child to truncate"); + + // If this is a comment node then we know that it is a `` + // text node separator that was created in virtual_node/mod.rs. + if child.node_type() == Node::COMMENT_NODE { + continue; + } + + non_separator_children_found += 1; + + if non_separator_children_found <= *num_children_remaining as u32 { + continue; + } + + node.remove_child(&child).expect("Truncated children"); + child_count -= 1; + } + + Ok(()) + // Ok(active_closures) + } + Patch::AppendChildren(_node_idx, new_nodes) => { + let parent = &node; + + let mut active_closures = HashMap::new(); + + for new_node in new_nodes { + let created_node = create_dom_node(&new_node); + // let created_node = new_node.create_dom_node(); + + parent.append_child(&created_node.node)?; + + active_closures.extend(created_node.closures); + } + + Ok(()) + // Ok(active_closures) + } + Patch::ChangeText(_node_idx, _new_node) => { + unreachable!("Elements should not receive ChangeText patches.") + } + } +} + +fn apply_text_patch(node: &Text, patch: &Patch) -> Result<(), JsValue> { + match patch { + Patch::ChangeText(_node_idx, new_node) => { + node.set_node_value(Some(&new_node.text)); + } + Patch::Replace(_node_idx, new_node) => { + node.replace_with_with_node_1(&create_dom_node(&new_node).node)?; + // node.replace_with_with_node_1(&new_node.create_dom_node().node)?; + } + other => unreachable!( + "Text nodes should only receive ChangeText or Replace patches, not ", + // other, + // "Text nodes should only receive ChangeText or Replace patches, not {:?}.", + // other, + ), + }; + + Ok(()) +} + +/// A node along with all of the closures that were created for that +/// node's events and all of it's child node's events. +pub struct CreatedNode { + /// A `Node` or `Element` that was created from a `VirtualNode` + pub node: T, + /// A map of a node's unique identifier along with all of the Closures for that node. + /// + /// The DomUpdater uses this to look up nodes and see if they're still in the page. If not + /// the reference that we maintain to their closure will be dropped, thus freeing the Closure's + /// memory. + pub closures: HashMap>, +} + +/// Box>> is our js_sys::Closure. Stored this way to allow us to store +/// any Closure regardless of the arguments. +pub type DynClosure = Rc>; + +impl CreatedNode { + pub fn without_closures>(node: N) -> Self { + CreatedNode { + node: node.into(), + closures: HashMap::with_capacity(0), + } + } +} + +impl std::ops::Deref for CreatedNode { + type Target = T; + fn deref(&self) -> &Self::Target { + &self.node + } +} + +impl From> for CreatedNode { + fn from(other: CreatedNode) -> CreatedNode { + CreatedNode { + node: other.node.into(), + closures: other.closures, + } + } +} +fn create_dom_node(node: &VNode<'_>) -> CreatedNode { + match node { + VNode::Text(text_node) => CreatedNode::without_closures(create_text_node(text_node)), + VNode::Element(element_node) => create_element_node(element_node).into(), + // VNode::Element(element_node) => element_node.create_element_node().into(), + VNode::Suspended => todo!(" not iimplemented yet"), + VNode::Component(_) => todo!(" not iimplemented yet"), + } +} + +/// Build a DOM element by recursively creating DOM nodes for this element and it's +/// children, it's children's children, etc. +pub fn create_element_node(node: &dioxus_core::nodes::VElement) -> CreatedNode { + let document = web_sys::window().unwrap().document().unwrap(); + + // TODO: enable svg again + // let element = if html_validation::is_svg_namespace(&node.tag_name) { + // document + // .create_element_ns(Some("http://www.w3.org/2000/svg"), &node.tag_name) + // .unwrap() + // } else { + let element = document.create_element(&node.tag_name).unwrap(); + // }; + + let mut closures = HashMap::new(); + + node.attributes + .iter() + .map(|f| (f.name, f.value)) + .for_each(|(name, value)| { + if name == "unsafe_inner_html" { + element.set_inner_html(value); + + return; + } + + element + .set_attribute(name, value) + .expect("Set element attribute in create element"); + }); + + // if node.events.0.len() > 0 { + // let unique_id = create_unique_identifier(); + + // element + // .set_attribute("data-vdom-id".into(), &unique_id.to_string()) + // .expect("Could not set attribute on element"); + + // closures.insert(unique_id, vec![]); + + // node.events.0.iter().for_each(|(onevent, callback)| { + // // onclick -> click + // let event = &onevent[2..]; + + // let current_elem: &EventTarget = element.dyn_ref().unwrap(); + + // current_elem + // .add_event_listener_with_callback(event, callback.as_ref().as_ref().unchecked_ref()) + // .unwrap(); + + // closures + // .get_mut(&unique_id) + // .unwrap() + // .push(Rc::clone(callback)); + // }); + // } + + let mut previous_node_was_text = false; + + node.children.iter().for_each(|child| { + // log::info!("Patching child"); + match child { + VNode::Text(text_node) => { + let current_node = element.as_ref() as &web_sys::Node; + + // 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 + if previous_node_was_text { + let separator = document.create_comment("ptns"); + current_node + .append_child(separator.as_ref() as &web_sys::Node) + .unwrap(); + } + + current_node + .append_child(&create_text_node(&text_node)) + .unwrap(); + + previous_node_was_text = true; + } + VNode::Element(element_node) => { + previous_node_was_text = false; + + let child = create_element_node(element_node); + // let child = element_node.create_element_node(); + let child_elem: Element = child.node; + + closures.extend(child.closures); + + element.append_child(&child_elem).unwrap(); + } + VNode::Suspended => { + todo!("Not yet supported") + } + VNode::Component(_) => { + todo!("Not yet supported") + } + } + }); + + // TODO: connect on mount to the event system somehow + // if let Some(on_create_elem) = node.events.0.get("on_create_elem") { + // let on_create_elem: &js_sys::Function = on_create_elem.as_ref().as_ref().unchecked_ref(); + // on_create_elem + // .call1(&wasm_bindgen::JsValue::NULL, &element) + // .unwrap(); + // } + + CreatedNode { + node: element, + closures, + } +} + +/// Return a `Text` element from a `VirtualNode`, typically right before adding it +/// into the DOM. +pub fn create_text_node(node: &VText) -> Text { + let document = web_sys::window().unwrap().document().unwrap(); + document.create_text_node(&node.text) +} + +// /// For any listeners in the tree, attach the sender closure. +// /// When a event is triggered, we convert it into the synthetic event type and dump it back in the Virtual Dom's queu +// fn attach_listeners(sender: &UnboundedSender, dom: &VirtualDom) {} +// fn render_diffs() {}