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!() + } +}