diff --git a/Cargo.toml b/Cargo.toml index a523e1fca..50a0bfbf6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,4 +1,3 @@ - [workspace] members = [ "packages/dioxus", @@ -13,17 +12,20 @@ members = [ "packages/mobile", "packages/interpreter", "packages/fermi", - "packages/tui", "packages/liveview", "packages/autofmt", "packages/rsx", + "docs/guide", + # "packages/tui", + # "packages/native-core", + # "packages/native-core-macro", +] +exclude = [ + "packages/tui", "packages/native-core", "packages/native-core-macro", - # "packages/edit-stream", - "docs/guide", ] - # This is a "virtual package" # It is not meant to be published, but is used so "cargo run --example XYZ" works properly [package] diff --git a/examples/router.rs b/examples/router.rs index 20b168670..a4118111a 100644 --- a/examples/router.rs +++ b/examples/router.rs @@ -30,7 +30,7 @@ fn app(cx: Scope) -> Element { } fn BlogPost(cx: Scope) -> Element { - let post = dioxus_router::use_route(&cx).last_segment()?; + let post = dioxus_router::use_route(&cx).last_segment().unwrap(); cx.render(rsx! { div { @@ -46,7 +46,7 @@ struct Query { } fn User(cx: Scope) -> Element { - let post = dioxus_router::use_route(&cx).last_segment()?; + let post = dioxus_router::use_route(&cx).last_segment().unwrap(); let query = dioxus_router::use_route(&cx) .query::() diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index 597c6ab79..99598479f 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -165,13 +165,13 @@ fn app(cx: Scope) -> Element { // Can pass in props directly as an expression { - let props = TallerProps {a: "hello", children: Default::default()}; + let props = TallerProps {a: "hello", children: cx.render(rsx!(()))}; rsx!(Taller { ..props }) } // Spreading can also be overridden manually Taller { - ..TallerProps { a: "ballin!", children: Default::default() }, + ..TallerProps { a: "ballin!", children: cx.render(rsx!(()) )}, a: "not ballin!" } diff --git a/examples/simple_todo.rs b/examples/simple_todo.rs new file mode 100644 index 000000000..454e4b508 --- /dev/null +++ b/examples/simple_todo.rs @@ -0,0 +1,32 @@ +use dioxus::prelude::*; + +fn main() { + dioxus_desktop::launch(app); +} + +fn app(cx: Scope) -> Element { + let mut idx = use_state(cx, || 0); + let onhover = |h| println!("go!"); + + cx.render(rsx! { + div { + button { onclick: move |_| idx += 1, "+" } + button { onclick: move |_| idx -= 1, "-" } + ul { + (0..**idx).map(|i| rsx! { + Child { i: i, onhover: onhover } + }) + } + } + }) +} + +#[inline_props] +fn Child<'a>(cx: Scope<'a>, i: i32, onhover: EventHandler<'a, MouseEvent>) -> Element { + cx.render(rsx! { + li { + onmouseover: move |e| onhover.call(e), + "{i}" + } + }) +} diff --git a/examples/todomvc.rs b/examples/todomvc.rs index 71ceee8e1..f74f56063 100644 --- a/examples/todomvc.rs +++ b/examples/todomvc.rs @@ -57,7 +57,10 @@ pub fn app(cx: Scope<()>) -> Element { placeholder: "What needs to be done?", value: "{draft}", autofocus: "true", - oninput: move |evt| draft.set(evt.value.clone()), + oninput: move |evt| { + println!("calling oninput"); + draft.set(evt.value.clone()); + }, onkeydown: move |evt| { if evt.key() == Key::Enter && !draft.is_empty() { todos.make_mut().insert( @@ -121,6 +124,8 @@ pub fn TodoEntry<'a>(cx: Scope<'a, TodoEntryProps<'a>>) -> Element { let completed = if todo.checked { "completed" } else { "" }; let editing = if **is_editing { "editing" } else { "" }; + println!("rendering todo entry"); + cx.render(rsx!{ li { class: "{completed} {editing}", diff --git a/packages/autofmt/src/buffer.rs b/packages/autofmt/src/buffer.rs index 720802346..45799ca62 100644 --- a/packages/autofmt/src/buffer.rs +++ b/packages/autofmt/src/buffer.rs @@ -67,7 +67,7 @@ impl Buffer { match node { BodyNode::Element(el) => self.write_element(el), BodyNode::Component(component) => self.write_component(component), - BodyNode::DynamicText(text) => self.write_text(text), + BodyNode::Text(text) => self.write_text(text), BodyNode::RawExpr(exp) => self.write_raw_expr(exp), _ => Ok(()), } diff --git a/packages/autofmt/src/element.rs b/packages/autofmt/src/element.rs index 4f14249c9..fd4529766 100644 --- a/packages/autofmt/src/element.rs +++ b/packages/autofmt/src/element.rs @@ -280,7 +280,7 @@ impl Buffer { } match children { - [BodyNode::DynamicText(ref text)] => Some(text.source.as_ref().unwrap().value().len()), + [BodyNode::Text(ref text)] => Some(text.source.as_ref().unwrap().value().len()), [BodyNode::Component(ref comp)] => { let attr_len = self.field_len(&comp.fields, &comp.manual_props); diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 55e0ff30a..f779e6ba2 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -42,4 +42,3 @@ dioxus = { path = "../dioxus" } [features] default = [] serialize = ["serde"] -debug_vdom = [] diff --git a/packages/core/README.md b/packages/core/README.md index 9b0b14fff..cf3467030 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -1,52 +1,84 @@ -# Dioxus-core +# dioxus-core -This is the core crate for the Dioxus Virtual DOM. This README will focus on the technical design and layout of this Virtual DOM implementation. If you want to read more about using Dioxus, then check out the Dioxus crate, documentation, and website. +dioxus-core is a fast and featureful VirtualDom implementation written in and for Rust. -To build new apps with Dioxus or to extend the ecosystem with new hooks or components, use the higher-level `dioxus` crate with the appropriate feature flags. +# Features +- Functions as components +- Hooks for local state +- Task pool for spawning futures +- Template-based architecture +- Asynchronous components +- Suspense boundaries +- Error boundaries through the `anyhow` crate +- Customizable memoization + +If just starting out, check out the Guides first. + +# General Theory + +The dioxus-core `VirtualDom` object is built around the concept of a `Template`. Templates describe a layout tree known at compile time with dynamic parts filled at runtime. + +Each component in the VirtualDom works as a dedicated render loop where re-renders are triggered by events external to the VirtualDom, or from the components themselves. + +When each component re-renders, it must return an `Element`. In Dioxus, the `Element` type is an alias for `Result`. Between two renders, Dioxus compares the inner `VNode` object, and calculates the differences of the dynamic portions of each internal `Template`. If any attributes or elements are different between the old layout and new layout, Dioxus will write modifications to the `Mutations` object. + +Dioxus expects the target renderer to save its nodes in a list. Each element is given a numerical ID which can be used to directly index into that list for O(1) lookups. + +# Usage + +All Dioxus apps start as just a function that takes the [`Scope`] object and returns an [`Element`]. + +The `dioxus` crate exports the `rsx` macro which transforms a helpful, simpler syntax of Rust into the logic required to build Templates. + +First, start with your app: ```rust, ignore fn app(cx: Scope) -> Element { - render!(div { "hello world" }) + cx.render(rsx!( div { "hello world" } )) } +``` -fn main() { - let mut renderer = SomeRenderer::new(); +Then, we'll want to create a new VirtualDom using this app as the root component. - // Creating a new virtualdom from a component - let mut dom = VirtualDom::new(app); +```rust, ingore +let mut dom = VirtualDom::new(app); +``` - // Patching the renderer with the changes to draw the screen - let edits = dom.rebuild(); - renderer.apply(edits); +To build the app into a stream of mutations, we'll use [`VirtualDom::rebuild`]: - // Injecting events - dom.handle_message(SchedulerMsg::Event(UserEvent { - scope_id: None, - priority: EventPriority::High, - element: ElementId(0), - name: "onclick", - data: Arc::new(()), - })); +```rust, ignore +let mutations = dom.rebuild(); - // polling asynchronously - dom.wait_for_work().await; +apply_edits_to_real_dom(mutations); +``` - // working with a deadline - if let Some(edits) = dom.work_with_deadline(|| false) { - renderer.apply(edits); +We can then wait for any asynchronous components or pending futures using the `wait_for_work()` method. If we have a deadline, then we can use render_with_deadline instead: + +```rust, ignore +// Wait for the dom to be marked dirty internally +dom.wait_for_work().await; + +// Or wait for a deadline and then collect edits +dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(16))); +``` + +If an event occurs from outside the virtualdom while waiting for work, then we can cancel the wait using a `select!` block and inject the event. + +```rust +loop { + tokio::select! { + evt = real_dom.event() => dom.handle_event("click", evt.data, evt.element, evt.bubbles), + _ = dom.wait_for_work() => {} } - // getting state of scopes - let scope = dom.get_scope(ScopeId(0)).unwrap(); + // Render any work without blocking the main thread for too long + let mutations = dom.render_with_deadline(tokio::time::sleep(Duration::from_millis(10))); - // iterating through the tree - match scope.root_node() { - VNodes::Text(vtext) => dbg!(vtext), - VNodes::Element(vel) => dbg!(vel), - _ => todo!() - } + // And then apply the edits + real_dom.apply(mutations); } + ``` ## Internals @@ -82,17 +114,3 @@ The final implementation of Dioxus must: - Support server-side-rendering (SSR). VNodes should render to a string that can be served via a web server. - Be "live". Components should be able to be both server rendered and client rendered without needing frontend APIs. - Be modular. Components and hooks should be work anywhere without worrying about target platform. - - -## Safety - -Dioxus uses unsafe. The design of Dioxus *requires* unsafe (self-referential trees). - -All of our test suite passes MIRI without errors. - -Dioxus deals with arenas, lifetimes, asynchronous tasks, custom allocators, pinning, and a lot more foundational low-level work that is very difficult to implement with 0 unsafe. - -If you don't want to use a crate that uses unsafe, then this crate is not for you. - -However, we are always interested in decreasing the scope of the core VirtualDom to make it easier to review. We'd be happy to welcome PRs that can eliminate unsafe code while still upholding the numerous invariants required to execute certain features. - diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index 532fe02ca..c3ea4dc2c 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,14 +1,18 @@ use std::marker::PhantomData; use crate::{ - factory::{ComponentReturn, RenderReturn}, innerlude::Scoped, + nodes::{ComponentReturn, RenderReturn}, scopes::{Scope, ScopeState}, Element, }; /// A trait that essentially allows VComponentProps to be used generically -pub unsafe trait AnyProps<'a> { +/// +/// # Safety +/// +/// This should not be implemented outside this module +pub(crate) unsafe trait AnyProps<'a> { fn props_ptr(&self) -> *const (); fn render(&'a self, bump: &'a ScopeState) -> RenderReturn<'a>; unsafe fn memoize(&self, other: &dyn AnyProps) -> bool; @@ -18,7 +22,6 @@ pub(crate) struct VProps<'a, P, A, F: ComponentReturn<'a, A> = Element<'a>> { pub render_fn: fn(Scope<'a, P>) -> F, pub memo: unsafe fn(&P, &P) -> bool, pub props: P, - // pub props: PropsAllocation

, _marker: PhantomData, } @@ -35,7 +38,6 @@ where render_fn, memo, props, - // props: PropsAllocation::Borrowed(props), _marker: PhantomData, } } @@ -60,12 +62,12 @@ where } fn render(&'a self, cx: &'a ScopeState) -> RenderReturn<'a> { - let scope = cx.bump().alloc(Scoped { + let scope: &mut Scoped

= cx.bump().alloc(Scoped { props: &self.props, scope: cx, }); // Call the render function directly - (self.render_fn)(scope).as_return(cx) + (self.render_fn)(scope).into_return(cx) } } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index c4fbe2b23..d6af9b051 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -1,47 +1,141 @@ -use crate::{nodes::VNode, virtual_dom::VirtualDom}; +use crate::{ + nodes::RenderReturn, nodes::VNode, virtual_dom::VirtualDom, AttributeValue, DynamicNode, + ScopeId, +}; +use bumpalo::boxed::Box as BumpBox; +/// An Element's unique identifier. +/// +/// `ElementId` is a `usize` that is unique across the entire VirtualDOM - but not unique across time. If a component is +/// unmounted, then the `ElementId` will be reused for a new component. #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct ElementId(pub usize); -pub struct ElementRef { +pub(crate) struct ElementRef { // the pathway of the real element inside the template - pub path: &'static [u8], + pub path: ElementPath, // The actual template - pub template: *mut VNode<'static>, + pub template: *const VNode<'static>, +} + +#[derive(Clone, Copy)] +pub enum ElementPath { + Deep(&'static [u8]), + Root(usize), } impl ElementRef { - pub fn null() -> Self { + pub(crate) fn null() -> Self { Self { template: std::ptr::null_mut(), - path: &[], + path: ElementPath::Root(0), } } } impl VirtualDom { - pub fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId { + pub(crate) fn next_element(&mut self, template: &VNode, path: &'static [u8]) -> ElementId { let entry = self.elements.vacant_entry(); let id = entry.key(); - entry.insert(ElementRef { template: template as *const _ as *mut _, - path, + path: ElementPath::Deep(path), }); - ElementId(id) } - pub fn cleanup_element(&mut self, id: ElementId) { - self.elements.remove(id.0); + pub(crate) fn next_root(&mut self, template: &VNode, path: usize) -> ElementId { + let entry = self.elements.vacant_entry(); + let id = entry.key(); + entry.insert(ElementRef { + template: template as *const _ as *mut _, + path: ElementPath::Root(path), + }); + ElementId(id) + } + + pub(crate) fn reclaim(&mut self, el: ElementId) { + self.try_reclaim(el) + .unwrap_or_else(|| panic!("cannot reclaim {:?}", el)); + } + + pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option { + if el.0 == 0 { + panic!( + "Invalid element set to 0 - {:#?}", + std::backtrace::Backtrace::force_capture() + ) + } + + println!("reclaiming {:?}", el); + self.elements.try_remove(el.0) + } + + pub(crate) fn update_template(&mut self, el: ElementId, node: &VNode) { + let node: *const VNode = node as *const _; + self.elements[el.0].template = unsafe { std::mem::transmute(node) }; + } + + // Drop a scope and all its children + pub(crate) fn drop_scope(&mut self, id: ScopeId) { + let scope = self.scopes.get(id.0).unwrap(); + + if let Some(root) = scope.as_ref().try_root_node() { + let root = unsafe { root.extend_lifetime_ref() }; + if let RenderReturn::Sync(Ok(node)) = root { + self.drop_scope_inner(node) + } + } + + let scope = self.scopes.get_mut(id.0).unwrap(); + scope.props.take(); + + // Drop all the hooks once the children are dropped + // this means we'll drop hooks bottom-up + for hook in scope.hook_list.get_mut().drain(..) { + println!("dropping hook !"); + drop(unsafe { BumpBox::from_raw(hook) }); + println!("hook dropped !"); + } + } + + fn drop_scope_inner(&mut self, node: &VNode) { + for attr in node.dynamic_attrs { + if let AttributeValue::Listener(l) = &attr.value { + l.borrow_mut().take(); + } + } + + for (idx, _) in node.template.roots.iter().enumerate() { + match node.dynamic_root(idx) { + Some(DynamicNode::Component(c)) => self.drop_scope(c.scope.get().unwrap()), + Some(DynamicNode::Fragment(nodes)) => { + for node in *nodes { + self.drop_scope_inner(node); + } + } + _ => {} + } + } } } -/* -now...... +impl ElementPath { + pub(crate) fn is_ascendant(&self, big: &&[u8]) -> bool { + match *self { + ElementPath::Deep(small) => small.len() <= big.len() && small == &big[..small.len()], + ElementPath::Root(r) => big.len() == 1 && big[0] == r as u8, + } + } +} -an ID is mostly a pointer to a node in the real dom. -We need to -*/ +impl PartialEq<&[u8]> for ElementPath { + fn eq(&self, other: &&[u8]) -> bool { + match *self { + ElementPath::Deep(deep) => deep.eq(*other), + ElementPath::Root(r) => other.len() == 1 && other[0] == r as u8, + } + } +} diff --git a/packages/core/src/bump_frame.rs b/packages/core/src/bump_frame.rs index ca7ff66f6..44ea4f2d6 100644 --- a/packages/core/src/bump_frame.rs +++ b/packages/core/src/bump_frame.rs @@ -1,28 +1,29 @@ -use crate::factory::RenderReturn; +use crate::nodes::RenderReturn; use bumpalo::Bump; use std::cell::Cell; -pub struct BumpFrame { +pub(crate) struct BumpFrame { pub bump: Bump, - pub node: Cell<*mut RenderReturn<'static>>, + pub node: Cell<*const RenderReturn<'static>>, } impl BumpFrame { - pub fn new(capacity: usize) -> Self { + pub(crate) fn new(capacity: usize) -> Self { let bump = Bump::with_capacity(capacity); Self { bump, - node: Cell::new(std::ptr::null_mut()), + node: Cell::new(std::ptr::null()), } } - pub fn reset(&mut self) { - self.bump.reset(); - self.node.set(std::ptr::null_mut()); - } - /// Creates a new lifetime out of thin air - pub unsafe fn load_node<'b>(&self) -> &'b RenderReturn<'b> { - unsafe { std::mem::transmute(&*self.node.get()) } + pub(crate) unsafe fn try_load_node<'b>(&self) -> Option<&'b RenderReturn<'b>> { + let node = self.node.get(); + + if node.is_null() { + return None; + } + + unsafe { std::mem::transmute(&*node) } } } diff --git a/packages/core/src/create.rs b/packages/core/src/create.rs index b0c1a10a9..feaa276e4 100644 --- a/packages/core/src/create.rs +++ b/packages/core/src/create.rs @@ -1,13 +1,14 @@ -use crate::factory::RenderReturn; -use crate::innerlude::{VComponent, VFragment, VText}; +use std::cell::Cell; + +use crate::innerlude::{VComponent, VText}; use crate::mutations::Mutation; use crate::mutations::Mutation::*; use crate::nodes::VNode; use crate::nodes::{DynamicNode, TemplateNode}; use crate::virtual_dom::VirtualDom; -use crate::{AttributeValue, ElementId, ScopeId, SuspenseContext, TemplateAttribute}; +use crate::{AttributeValue, ElementId, RenderReturn, ScopeId, SuspenseContext}; -impl<'b: 'static> VirtualDom { +impl<'b> VirtualDom { /// Create a new template [`VNode`] and write it to the [`Mutations`] buffer. /// /// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created. @@ -23,7 +24,7 @@ impl<'b: 'static> VirtualDom { pub(crate) fn create(&mut self, template: &'b VNode<'b>) -> usize { // The best renderers will have templates prehydrated and registered // Just in case, let's create the template using instructions anyways - if !self.templates.contains_key(&template.template.id) { + if !self.templates.contains_key(&template.template.name) { self.register_template(template); } @@ -44,29 +45,36 @@ impl<'b: 'static> VirtualDom { DynamicNode::Text(VText { id: slot, value }) => { let id = self.next_element(template, template.template.node_paths[*id]); slot.set(id); - self.mutations.push(CreateTextNode { value, id }); + + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let unbounded_text = unsafe { std::mem::transmute(*value) }; + self.mutations.push(CreateTextNode { + value: unbounded_text, + id, + }); + 1 } - DynamicNode::Fragment(VFragment::Empty(slot)) => { + DynamicNode::Placeholder(slot) => { let id = self.next_element(template, template.template.node_paths[*id]); slot.set(id); self.mutations.push(CreatePlaceholder { id }); 1 } - DynamicNode::Fragment(VFragment::NonEmpty(_)) - | DynamicNode::Component { .. } => { + DynamicNode::Fragment(_) | DynamicNode::Component { .. } => { self.create_dynamic_node(template, &template.dynamic_nodes[*id], *id) } } } TemplateNode::Element { .. } | TemplateNode::Text(_) => { - let this_id = self.next_element(template, &[]); + let this_id = self.next_root(template, root_idx); + template.root_ids[root_idx].set(this_id); self.mutations.push(LoadTemplate { - name: template.template.id, + name: template.template.name, index: root_idx, id: this_id, }); @@ -80,23 +88,39 @@ impl<'b: 'static> VirtualDom { // Else, it's deep in the template and we should create a new id for it let id = match path.len() { 1 => this_id, - _ => self.next_element(template, template.template.attr_paths[attr_id]), + _ => { + let id = self + .next_element(template, template.template.attr_paths[attr_id]); + self.mutations.push(Mutation::AssignId { + path: &path[1..], + id, + }); + id + } }; loop { let attribute = template.dynamic_attrs.get(attr_id).unwrap(); attribute.mounted_element.set(id); + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let unbounded_name = unsafe { std::mem::transmute(attribute.name) }; + match &attribute.value { - AttributeValue::Text(value) => self.mutations.push(SetAttribute { - name: attribute.name, - value: *value, - ns: attribute.namespace, - id, - }), + AttributeValue::Text(value) => { + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let unbounded_value = unsafe { std::mem::transmute(*value) }; + + self.mutations.push(SetAttribute { + name: unbounded_name, + value: unbounded_value, + ns: attribute.namespace, + id, + }) + } AttributeValue::Bool(value) => { self.mutations.push(SetBoolAttribute { - name: attribute.name, + name: unbounded_name, value: *value, id, }) @@ -104,7 +128,7 @@ impl<'b: 'static> VirtualDom { AttributeValue::Listener(_) => { self.mutations.push(NewEventListener { // all listeners start with "on" - event_name: &attribute.name[2..], + name: &unbounded_name[2..], scope: cur_scope, id, }) @@ -179,7 +203,7 @@ impl<'b: 'static> VirtualDom { fn register_template(&mut self, template: &'b VNode<'b>) { // First, make sure we mark the template as seen, regardless if we process it self.templates - .insert(template.template.id, template.template); + .insert(template.template.name, template.template); // If it's all dynamic nodes, then we don't need to register it // Quickly run through and see if it's all just dynamic nodes @@ -199,85 +223,7 @@ impl<'b: 'static> VirtualDom { return; } - for node in template.template.roots { - self.create_static_node(template, node); - } - - self.mutations.template_mutations.push(SaveTemplate { - name: template.template.id, - m: template.template.roots.len(), - }); - } - - pub(crate) fn create_static_node( - &mut self, - template: &'b VNode<'b>, - node: &'b TemplateNode<'static>, - ) { - match *node { - // Todo: create the children's template - TemplateNode::Dynamic(_) => self - .mutations - .template_mutations - .push(CreateStaticPlaceholder {}), - TemplateNode::Text(value) => self - .mutations - .template_mutations - .push(CreateStaticText { value }), - TemplateNode::DynamicText { .. } => self - .mutations - .template_mutations - .push(CreateStaticText { value: "d" }), - - TemplateNode::Element { - attrs, - children, - namespace, - tag, - .. - } => { - if let Some(namespace) = namespace { - self.mutations - .template_mutations - .push(CreateElementNamespace { - name: tag, - namespace, - }); - } else { - self.mutations - .template_mutations - .push(CreateElement { name: tag }); - } - - self.mutations - .template_mutations - .extend(attrs.into_iter().filter_map(|attr| match attr { - TemplateAttribute::Static { - name, - value, - namespace, - .. - } => Some(SetStaticAttribute { - name, - value, - ns: *namespace, - }), - _ => None, - })); - - if children.is_empty() { - return; - } - - children - .into_iter() - .for_each(|child| self.create_static_node(template, child)); - - self.mutations - .template_mutations - .push(AppendChildren { m: children.len() }) - } - } + self.mutations.templates.push(template.template); } pub(crate) fn create_dynamic_node( @@ -289,7 +235,8 @@ impl<'b: 'static> VirtualDom { use DynamicNode::*; match node { Text(text) => self.create_dynamic_text(template, text, idx), - Fragment(frag) => self.create_fragment(frag, template, idx), + Fragment(frag) => self.create_fragment(frag), + Placeholder(frag) => self.create_placeholder(frag, template, idx), Component(component) => self.create_component_node(template, component, idx), } } @@ -306,59 +253,60 @@ impl<'b: 'static> VirtualDom { // Make sure the text node is assigned to the correct element text.id.set(new_id); + // Safety: we promise not to re-alias this text later on after committing it to the mutation + let value = unsafe { std::mem::transmute(text.value) }; + // Add the mutation to the list self.mutations.push(HydrateText { id: new_id, path: &template.template.node_paths[idx][1..], - value: text.value, + value, }); // Since we're hydrating an existing node, we don't create any new nodes 0 } - pub(crate) fn create_fragment( + pub(crate) fn create_placeholder( &mut self, - frag: &'b VFragment<'b>, + slot: &Cell, template: &'b VNode<'b>, idx: usize, ) -> usize { - match frag { - VFragment::NonEmpty(nodes) => { - nodes.iter().fold(0, |acc, child| acc + self.create(child)) - } + // Allocate a dynamic element reference for this text node + let id = self.next_element(template, template.template.node_paths[idx]); - VFragment::Empty(slot) => { - // Allocate a dynamic element reference for this text node - let id = self.next_element(template, template.template.node_paths[idx]); + // Make sure the text node is assigned to the correct element + slot.set(id); - // Make sure the text node is assigned to the correct element - slot.set(id); + // Assign the ID to the existing node in the template + self.mutations.push(AssignId { + path: &template.template.node_paths[idx][1..], + id, + }); - // Assign the ID to the existing node in the template - self.mutations.push(AssignId { - path: &template.template.node_paths[idx][1..], - id, - }); - - // Since the placeholder is already in the DOM, we don't create any new nodes - 0 - } - } + // Since the placeholder is already in the DOM, we don't create any new nodes + 0 } - fn create_component_node( + pub(crate) fn create_fragment(&mut self, nodes: &'b [VNode<'b>]) -> usize { + nodes.iter().fold(0, |acc, child| acc + self.create(child)) + } + + pub(super) fn create_component_node( &mut self, template: &'b VNode<'b>, component: &'b VComponent<'b>, idx: usize, ) -> usize { - let props = component.props.replace(None).unwrap(); + let props = component + .props + .replace(None) + .expect("Props to always exist when a component is being created"); - let prop_ptr = unsafe { std::mem::transmute(props.as_ref()) }; - let scope = self.new_scope(prop_ptr).id; + let unbounded_props = unsafe { std::mem::transmute(props) }; - component.props.replace(Some(props)); + let scope = self.new_scope(unbounded_props).id; component.scope.set(Some(scope)); let return_nodes = unsafe { self.run_scope(scope).extend_lifetime_ref() }; @@ -366,10 +314,7 @@ impl<'b: 'static> VirtualDom { use RenderReturn::*; match return_nodes { - Sync(Ok(t)) => { - self.mount_component(scope, template, t, idx) - // self.create(t) - } + Sync(Ok(t)) => self.mount_component(scope, template, t, idx), Sync(Err(_e)) => todo!("Propogate error upwards"), Async(_) => self.mount_component_placeholder(template, idx, scope), } @@ -384,7 +329,7 @@ impl<'b: 'static> VirtualDom { ) -> usize { // Keep track of how many mutations are in the buffer in case we need to split them out if a suspense boundary // is encountered - let mutations_to_this_point = self.mutations.len(); + let mutations_to_this_point = self.mutations.edits.len(); // Create the component's root element let created = self.create_scope(scope, new); @@ -396,7 +341,7 @@ impl<'b: 'static> VirtualDom { // If running the scope has collected some leaves and *this* component is a boundary, then handle the suspense let boundary = match self.scopes[scope.0].has_context::() { - Some(boundary) => boundary, + Some(boundary) => unsafe { &*(boundary as *const SuspenseContext) }, _ => return created, }; @@ -410,7 +355,7 @@ impl<'b: 'static> VirtualDom { // Note that we break off dynamic mutations only - since static mutations aren't rendered immediately let split_off = unsafe { std::mem::transmute::, Vec>( - self.mutations.split_off(mutations_to_this_point), + self.mutations.edits.split_off(mutations_to_this_point), ) }; boundary.mutations.borrow_mut().edits.extend(split_off); diff --git a/packages/core/src/diff.rs b/packages/core/src/diff.rs index 98d7dc56b..c0b14d0bd 100644 --- a/packages/core/src/diff.rs +++ b/packages/core/src/diff.rs @@ -1,8 +1,10 @@ +use std::cell::Cell; + use crate::{ arena::ElementId, - factory::RenderReturn, - innerlude::{DirtyScope, VComponent, VFragment, VText}, + innerlude::{DirtyScope, VComponent, VText}, mutations::Mutation, + nodes::RenderReturn, nodes::{DynamicNode, VNode}, scopes::ScopeId, virtual_dom::VirtualDom, @@ -10,71 +12,66 @@ use crate::{ }; use fxhash::{FxHashMap, FxHashSet}; -use std::cell::Cell; use DynamicNode::*; -impl<'b: 'static> VirtualDom { - pub fn diff_scope(&mut self, scope: ScopeId) { +impl<'b> VirtualDom { + pub(super) fn diff_scope(&mut self, scope: ScopeId) { let scope_state = &mut self.scopes[scope.0]; - // Load the old and new bump arenas - let cur_arena = scope_state.current_frame(); - let prev_arena = scope_state.previous_frame(); - - // Make sure the nodes arent null (they've been set properly) - // This is a rough check to make sure we're not entering any UB - assert_ne!( - cur_arena.node.get(), - std::ptr::null_mut(), - "Call rebuild before diffing" - ); - assert_ne!( - prev_arena.node.get(), - std::ptr::null_mut(), - "Call rebuild before diffing" - ); - self.scope_stack.push(scope); unsafe { - let cur_arena = cur_arena.load_node(); - let prev_arena = prev_arena.load_node(); - self.diff_maybe_node(prev_arena, cur_arena); + // Load the old and new bump arenas + let old = scope_state + .previous_frame() + .try_load_node() + .expect("Call rebuild before diffing"); + let new = scope_state + .current_frame() + .try_load_node() + .expect("Call rebuild before diffing"); + self.diff_maybe_node(old, new); } self.scope_stack.pop(); } - fn diff_maybe_node(&mut self, left: &'b RenderReturn<'b>, right: &'b RenderReturn<'b>) { + fn diff_maybe_node(&mut self, old: &'b RenderReturn<'b>, new: &'b RenderReturn<'b>) { use RenderReturn::{Async, Sync}; - match (left, right) { - // diff + match (old, new) { (Sync(Ok(l)), Sync(Ok(r))) => self.diff_node(l, r), - _ => todo!("handle diffing nonstandard nodes"), - // // remove old with placeholder - // (Sync(Ok(l)), Sync(None)) | (Sync(Ok(l)), Async(_)) => { - // // - // let id = self.next_element(l, &[]); // todo! - // m.push(Mutation::CreatePlaceholder { id }); - // self.drop_template(m, l, true); - // } + // Err cases + (Sync(Ok(l)), Sync(Err(e))) => self.diff_ok_to_err(l, e), + (Sync(Err(e)), Sync(Ok(r))) => self.diff_err_to_ok(e, r), + (Sync(Err(_eo)), Sync(Err(_en))) => { /* nothing */ } - // // remove placeholder with nodes - // (Sync(None), Sync(Ok(_))) => {} - // (Async(_), Sync(Ok(v))) => {} - - // // nothing... just transfer the placeholders over - // (Async(_), Async(_)) - // | (Sync(None), Sync(None)) - // | (Sync(None), Async(_)) - // | (Async(_), Sync(None)) => {} + // Async + (Sync(Ok(_l)), Async(_)) => todo!(), + (Sync(Err(_e)), Async(_)) => todo!(), + (Async(_), Sync(Ok(_r))) => todo!(), + (Async(_), Sync(Err(_e))) => { /* nothing */ } + (Async(_), Async(_)) => { /* nothing */ } } } - pub fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { - if left_template.template.id != right_template.template.id { + fn diff_ok_to_err(&mut self, _l: &'b VNode<'b>, _e: &anyhow::Error) { + todo!("Not yet handling error rollover") + } + fn diff_err_to_ok(&mut self, _e: &anyhow::Error, _l: &'b VNode<'b>) { + todo!("Not yet handling error rollover") + } + + fn diff_node(&mut self, left_template: &'b VNode<'b>, right_template: &'b VNode<'b>) { + println!( + "diffing node {:#?},\n\n{:#?}", + left_template.template.name, right_template.template.name + ); + + if left_template.template.name != right_template.template.name { return self.light_diff_templates(left_template, right_template); } + println!("diffing attributs..."); + for (left_attr, right_attr) in left_template .dynamic_attrs .iter() @@ -85,36 +82,59 @@ impl<'b: 'static> VirtualDom { .mounted_element .set(left_attr.mounted_element.get()); + // We want to make sure anything listener that gets pulled is valid + if let AttributeValue::Listener(_) = right_attr.value { + self.update_template(left_attr.mounted_element.get(), right_template); + } + if left_attr.value != right_attr.value || left_attr.volatile { // todo: add more types of attribute values match right_attr.value { AttributeValue::Text(text) => { + let name = unsafe { std::mem::transmute(left_attr.name) }; + let value = unsafe { std::mem::transmute(text) }; self.mutations.push(Mutation::SetAttribute { id: left_attr.mounted_element.get(), - name: left_attr.name, - value: text, ns: right_attr.namespace, + name, + value, }); } // todo: more types of attribute values - _ => (), + _ => todo!("other attribute types"), } } } - for (left_node, right_node) in left_template + println!("diffing dyn nodes..."); + + for (idx, (left_node, right_node)) in left_template .dynamic_nodes .iter() .zip(right_template.dynamic_nodes.iter()) + .enumerate() { match (left_node, right_node) { (Text(left), Text(right)) => self.diff_vtext(left, right), - (Fragment(left), Fragment(right)) => self.diff_vfragment(left, right), - (Component(left), Component(right)) => self.diff_vcomponent(left, right), - _ => self.replace(left_template, right_template, left_node, right_node), + (Fragment(left), Fragment(right)) => self.diff_non_empty_fragment(left, right), + (Placeholder(left), Placeholder(right)) => { + right.set(left.get()); + } + (Component(left), Component(right)) => { + self.diff_vcomponent(left, right, right_template, idx) + } + (Placeholder(left), Fragment(right)) => { + self.replace_placeholder_with_nodes(left, right) + } + (Fragment(left), Placeholder(right)) => { + self.replace_nodes_with_placeholder(left, right) + } + _ => todo!(), }; } + println!("applying roots..."); + // Make sure the roots get transferred over for (left, right) in left_template .root_ids @@ -125,31 +145,73 @@ impl<'b: 'static> VirtualDom { } } - fn diff_vcomponent(&mut self, left: &'b VComponent<'b>, right: &'b VComponent<'b>) { - // Due to how templates work, we should never get two different components. The only way we could enter - // this codepath is through "light_diff", but we check there that the pointers are the same - assert_eq!(left.render_fn, right.render_fn); + fn replace_placeholder_with_nodes(&mut self, l: &'b Cell, r: &'b [VNode<'b>]) { + let m = self.create_children(r); + let id = l.get(); + self.mutations.push(Mutation::ReplaceWith { id, m }); + self.reclaim(id); + } + + fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell) { + // Create the placeholder first, ensuring we get a dedicated ID for the placeholder + let placeholder = self.next_element(&l[0], &[]); + r.set(placeholder); + self.mutations + .push(Mutation::CreatePlaceholder { id: placeholder }); + + // Remove the old nodes, except for onea + let first = self.replace_inner(&l[0]); + self.remove_nodes(&l[1..]); + + self.mutations + .push(Mutation::ReplaceWith { id: first, m: 1 }); + + self.try_reclaim(first); + } + + fn diff_vcomponent( + &mut self, + left: &'b VComponent<'b>, + right: &'b VComponent<'b>, + right_template: &'b VNode<'b>, + idx: usize, + ) { + // Replace components that have different render fns + if left.render_fn != right.render_fn { + dbg!(&left, &right); + let created = self.create_component_node(right_template, right, idx); + let head = unsafe { + self.scopes[left.scope.get().unwrap().0] + .root_node() + .extend_lifetime_ref() + }; + let id = match head { + RenderReturn::Sync(Ok(node)) => self.replace_inner(node), + _ => todo!(), + }; + self.mutations + .push(Mutation::ReplaceWith { id, m: created }); + self.drop_scope(left.scope.get().unwrap()); + return; + } // Make sure the new vcomponent has the right scopeid associated to it let scope_id = left.scope.get().unwrap(); right.scope.set(Some(scope_id)); // copy out the box for both - let old = left.props.replace(None).unwrap(); + let old = self.scopes[scope_id.0].props.as_ref(); let new = right.props.replace(None).unwrap(); // If the props are static, then we try to memoize by setting the new with the old // The target scopestate still has the reference to the old props, so there's no need to update anything // This also implicitly drops the new props since they're not used - if left.static_props && unsafe { old.memoize(new.as_ref()) } { - return right.props.set(Some(old)); + if left.static_props && unsafe { old.as_ref().unwrap().memoize(new.as_ref()) } { + return; } - // If the props are dynamic *or* the memoization failed, then we need to diff the props - // First, move over the props from the old to the new, dropping old props in the process - self.scopes[scope_id.0].props = unsafe { std::mem::transmute(new.as_ref()) }; - right.props.set(Some(new)); + self.scopes[scope_id.0].props = unsafe { std::mem::transmute(new) }; // Now run the component and diff it self.run_scope(scope_id); @@ -171,7 +233,7 @@ impl<'b: 'static> VirtualDom { /// This is mostly implemented to help solve the issue where the same component is rendered under two different /// conditions: /// - /// ```rust + /// ```rust, ignore /// if enabled { /// rsx!{ Component { enabled_sign: "abc" } } /// } else { @@ -182,8 +244,7 @@ impl<'b: 'static> VirtualDom { /// However, we should not that it's explicit in the docs that this is not a guarantee. If you need to preserve state, /// then you should be passing in separate props instead. /// - /// ``` - /// + /// ```rust, ignore /// let props = if enabled { /// ComponentProps { enabled_sign: "abc" } /// } else { @@ -196,10 +257,11 @@ impl<'b: 'static> VirtualDom { /// ``` fn light_diff_templates(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) { match matching_components(left, right) { - None => self.replace_template(left, right), + None => self.replace(left, right), Some(components) => components .into_iter() - .for_each(|(l, r)| self.diff_vcomponent(l, r)), + .enumerate() + .for_each(|(idx, (l, r))| self.diff_vcomponent(l, r, right, idx)), } } @@ -208,106 +270,121 @@ impl<'b: 'static> VirtualDom { /// This just moves the ID of the old node over to the new node, and then sets the text of the new node if it's /// different. fn diff_vtext(&mut self, left: &'b VText<'b>, right: &'b VText<'b>) { - right.id.set(left.id.get()); + let id = left.id.get(); + + right.id.set(id); if left.value != right.value { - self.mutations.push(Mutation::SetText { - id: left.id.get(), - value: right.value, - }); + let value = unsafe { std::mem::transmute(right.value) }; + self.mutations.push(Mutation::SetText { id, value }); } } - fn diff_vfragment(&mut self, left: &'b VFragment<'b>, right: &'b VFragment<'b>) { - use VFragment::*; - match (left, right) { - (Empty(l), Empty(r)) => r.set(l.get()), - (Empty(l), NonEmpty(r)) => self.replace_placeholder_with_nodes(l, r), - (NonEmpty(l), Empty(r)) => self.replace_nodes_with_placeholder(l, r), - (NonEmpty(old), NonEmpty(new)) => self.diff_non_empty_fragment(new, old), - } - } - - fn replace_placeholder_with_nodes(&mut self, l: &'b Cell, r: &'b [VNode<'b>]) { - let created = self.create_children(r); - self.mutations.push(Mutation::ReplaceWith { - id: l.get(), - m: created, - }) - } - - fn replace_nodes_with_placeholder(&mut self, l: &'b [VNode<'b>], r: &'b Cell) { - // - // Remove the old nodes, except for one - self.remove_nodes(&l[1..]); - let first = self.find_first_element(&l[0]); - self.remove_all_but_first_node(&l[0]); - - let placeholder = self.next_element(&l[0], &[]); - r.set(placeholder); - self.mutations - .push(Mutation::CreatePlaceholder { id: placeholder }); - self.mutations - .push(Mutation::ReplaceWith { id: first, m: 1 }) - } - - // Remove all the top-level nodes, returning the - fn remove_all_but_first_node(&mut self, node: &'b VNode<'b>) { - match &node.template.roots[0] { - TemplateNode::Text(_) | TemplateNode::Element { .. } => {} - TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => { - match &node.dynamic_nodes[*id] { - Text(_) => {} - Fragment(VFragment::Empty(_)) => {} - Fragment(VFragment::NonEmpty(nodes)) => { - self.remove_all_but_first_node(&nodes[0]); - self.remove_nodes(&nodes[1..]); - } - Component(comp) => { - let scope = comp.scope.get().unwrap(); - match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Ok(t)) => self.remove_all_but_first_node(t), - _ => todo!("cannot handle nonstandard nodes"), - }; - } - }; + /// Remove all the top-level nodes, returning the firstmost root ElementId + /// + /// All IDs will be garbage collected + fn replace_inner(&mut self, node: &'b VNode<'b>) -> ElementId { + let id = match node.dynamic_root(0) { + None => node.root_ids[0].get(), + Some(Text(t)) => t.id.get(), + Some(Placeholder(e)) => e.get(), + Some(Fragment(nodes)) => { + let id = self.replace_inner(&nodes[0]); + self.remove_nodes(&nodes[1..]); + id } - } + Some(Component(comp)) => { + let scope = comp.scope.get().unwrap(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.replace_inner(t), + _ => todo!("cannot handle nonstandard nodes"), + } + } + }; - // Just remove the rest + // Just remove the rest from the dom for (idx, _) in node.template.roots.iter().enumerate().skip(1) { self.remove_root_node(node, idx); } + + // Garabge collect all of the nodes since this gets used in replace + self.clean_up_node(node); + + id + } + + /// Clean up the node, not generating mutations + /// + /// Simply walks through the dynamic nodes + fn clean_up_node(&mut self, node: &'b VNode<'b>) { + for (idx, dyn_node) in node.dynamic_nodes.iter().enumerate() { + // Roots are cleaned up automatically? + if node.template.node_paths[idx].len() == 1 { + continue; + } + + match dyn_node { + Component(comp) => { + let scope = comp.scope.get().unwrap(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.clean_up_node(t), + _ => todo!("cannot handle nonstandard nodes"), + }; + } + Text(t) => self.reclaim(t.id.get()), + Placeholder(t) => self.reclaim(t.get()), + Fragment(nodes) => nodes.iter().for_each(|node| self.clean_up_node(node)), + }; + } + + // we clean up nodes with dynamic attributes, provided the node is unique and not a root node + let mut id = None; + for (idx, attr) in node.dynamic_attrs.iter().enumerate() { + // We'll clean up the root nodes either way, so don't worry + if node.template.attr_paths[idx].len() == 1 { + continue; + } + + let next_id = attr.mounted_element.get(); + + if id == Some(next_id) { + continue; + } + + id = Some(next_id); + + self.reclaim(next_id); + } } fn remove_root_node(&mut self, node: &'b VNode<'b>, idx: usize) { - let root = node.template.roots[idx]; - match root { - TemplateNode::Element { .. } | TemplateNode::Text(_) => { - self.mutations.push(Mutation::Remove { - id: node.root_ids[idx].get(), - }) + match node.dynamic_root(idx) { + Some(Text(i)) => { + let id = i.id.get(); + self.mutations.push(Mutation::Remove { id }); + self.reclaim(id); } - - TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => { - match &node.dynamic_nodes[id] { - Text(i) => self.mutations.push(Mutation::Remove { id: i.id.get() }), - Fragment(VFragment::Empty(e)) => { - self.mutations.push(Mutation::Remove { id: e.get() }) - } - Fragment(VFragment::NonEmpty(nodes)) => self.remove_nodes(nodes), - Component(comp) => { - let scope = comp.scope.get().unwrap(); - match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { - RenderReturn::Sync(Ok(t)) => self.remove_node(t), - _ => todo!("cannot handle nonstandard nodes"), - }; - } + Some(Placeholder(e)) => { + let id = e.get(); + self.mutations.push(Mutation::Remove { id }); + self.reclaim(id); + } + Some(Fragment(nodes)) => self.remove_nodes(nodes), + Some(Component(comp)) => { + let scope = comp.scope.get().unwrap(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.remove_node(t), + _ => todo!("cannot handle nonstandard nodes"), }; } - } + None => { + let id = node.root_ids[idx].get(); + self.mutations.push(Mutation::Remove { id }); + self.reclaim(id); + } + }; } - fn diff_non_empty_fragment(&mut self, new: &'b [VNode<'b>], old: &'b [VNode<'b>]) { + fn diff_non_empty_fragment(&mut self, old: &'b [VNode<'b>], new: &'b [VNode<'b>]) { let new_is_keyed = new[0].key.is_some(); let old_is_keyed = old[0].key.is_some(); debug_assert!( @@ -318,6 +395,9 @@ impl<'b: 'static> VirtualDom { old.iter().all(|o| o.key.is_some() == old_is_keyed), "all siblings must be keyed or all siblings must be non-keyed" ); + + println!("Diffing fragment {:?}, {:?}", old.len(), new.len()); + if new_is_keyed && old_is_keyed { self.diff_keyed_children(old, new); } else { @@ -340,7 +420,9 @@ impl<'b: 'static> VirtualDom { debug_assert!(!new.is_empty()); debug_assert!(!old.is_empty()); - match old.len().cmp(&new.len()) { + println!("Diffing non keyed children"); + + match dbg!(old.len().cmp(&new.len())) { Ordering::Greater => self.remove_nodes(&old[new.len()..]), Ordering::Less => self.create_and_insert_after(&new[old.len()..], old.last().unwrap()), Ordering::Equal => {} @@ -554,16 +636,15 @@ impl<'b: 'static> VirtualDom { // If none of the old keys are reused by the new children, then we remove all the remaining old children and // create the new children afresh. if shared_keys.is_empty() { - if let Some(first_old) = old.get(0) { + if old.get(0).is_some() { self.remove_nodes(&old[1..]); - let nodes_created = self.create_children(new); - self.replace_node_with_on_stack(first_old, nodes_created); + self.replace_many(&old[0], new); } else { // I think this is wrong - why are we appending? // only valid of the if there are no trailing elements // self.create_and_append_children(new); - todo!() + todo!("we should never be appending - just creating N"); } return; } @@ -573,8 +654,7 @@ impl<'b: 'static> VirtualDom { for child in old { let key = child.key.unwrap(); if !shared_keys.contains(&key) { - todo!("remove node"); - // self.remove_nodes( [child]); + self.remove_node(child); } } @@ -677,39 +757,33 @@ impl<'b: 'static> VirtualDom { } } - fn insert_after(&mut self, node: &'b VNode<'b>, nodes_created: usize) { - todo!() - } - fn insert_before(&mut self, node: &'b VNode<'b>, nodes_created: usize) { - todo!() - } - - fn replace_node_with_on_stack(&mut self, old: &'b VNode<'b>, m: usize) { - todo!() - } - /// Remove these nodes from the dom /// Wont generate mutations for the inner nodes fn remove_nodes(&mut self, nodes: &'b [VNode<'b>]) { - nodes.into_iter().for_each(|node| self.remove_node(node)); + nodes.iter().for_each(|node| self.remove_node(node)); } fn remove_node(&mut self, node: &'b VNode<'b>) { - for (idx, root) in node.template.roots.iter().enumerate() { - let id = match root { - TemplateNode::Text(_) | TemplateNode::Element { .. } => node.root_ids[idx].get(), - TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => { - match &node.dynamic_nodes[*id] { - Text(t) => t.id.get(), - Fragment(VFragment::Empty(t)) => t.get(), - Fragment(VFragment::NonEmpty(t)) => return self.remove_nodes(t), - Component(comp) => return self.remove_component(comp.scope.get().unwrap()), - } - } + for (idx, _) in node.template.roots.iter().enumerate() { + let id = match node.dynamic_root(idx) { + Some(Text(t)) => t.id.get(), + Some(Placeholder(t)) => t.get(), + Some(Fragment(t)) => return self.remove_nodes(t), + Some(Component(comp)) => return self.remove_component(comp.scope.get().unwrap()), + None => node.root_ids[idx].get(), }; self.mutations.push(Mutation::Remove { id }) } + + self.clean_up_node(node); + + for root in node.root_ids { + let id = root.get(); + if id.0 != 0 { + self.reclaim(id); + } + } } fn remove_component(&mut self, scope_id: ScopeId) { @@ -727,8 +801,42 @@ impl<'b: 'static> VirtualDom { } /// Push all the real nodes on the stack - fn push_all_real_nodes(&mut self, node: &VNode) -> usize { - todo!() + fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize { + let mut onstack = 0; + + for (idx, _) in node.template.roots.iter().enumerate() { + match node.dynamic_root(idx) { + Some(Text(t)) => { + self.mutations.push(Mutation::PushRoot { id: t.id.get() }); + onstack += 1; + } + Some(Placeholder(t)) => { + self.mutations.push(Mutation::PushRoot { id: t.get() }); + onstack += 1; + } + Some(Fragment(nodes)) => { + for node in *nodes { + onstack += self.push_all_real_nodes(node); + } + } + Some(Component(comp)) => { + let scope = comp.scope.get().unwrap(); + onstack += + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(node)) => self.push_all_real_nodes(node), + _ => todo!(), + } + } + None => { + self.mutations.push(Mutation::PushRoot { + id: node.root_ids[idx].get(), + }); + onstack += 1; + } + }; + } + + onstack } fn create_children(&mut self, nodes: &'b [VNode<'b>]) -> usize { @@ -747,68 +855,58 @@ impl<'b: 'static> VirtualDom { self.mutations.push(Mutation::InsertAfter { id, m }) } - fn find_first_element(&self, node: &VNode) -> ElementId { - match &node.template.roots[0] { - TemplateNode::Element { .. } | TemplateNode::Text(_) => node.root_ids[0].get(), - TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => { - match &node.dynamic_nodes[*id] { - Text(t) => t.id.get(), - Fragment(VFragment::NonEmpty(t)) => self.find_first_element(&t[0]), - Fragment(VFragment::Empty(t)) => t.get(), - Component(comp) => { - let scope = comp.scope.get().unwrap(); - match self.scopes[scope.0].root_node() { - RenderReturn::Sync(Ok(t)) => self.find_first_element(t), - _ => todo!("cannot handle nonstandard nodes"), - } - } + fn find_first_element(&self, node: &'b VNode<'b>) -> ElementId { + match node.dynamic_root(0) { + None => node.root_ids[0].get(), + Some(Text(t)) => t.id.get(), + Some(Fragment(t)) => self.find_first_element(&t[0]), + Some(Placeholder(t)) => t.get(), + Some(Component(comp)) => { + let scope = comp.scope.get().unwrap(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.find_first_element(t), + _ => todo!("cannot handle nonstandard nodes"), } } } } - fn find_last_element(&self, node: &VNode) -> ElementId { - match node.template.roots.last().unwrap() { - TemplateNode::Element { .. } | TemplateNode::Text(_) => { - node.root_ids.last().unwrap().get() - } - TemplateNode::DynamicText(id) | TemplateNode::Dynamic(id) => { - match &node.dynamic_nodes[*id] { - Text(t) => t.id.get(), - Fragment(VFragment::NonEmpty(t)) => self.find_last_element(t.last().unwrap()), - Fragment(VFragment::Empty(t)) => t.get(), - Component(comp) => { - let scope = comp.scope.get().unwrap(); - match self.scopes[scope.0].root_node() { - RenderReturn::Sync(Ok(t)) => self.find_last_element(t), - _ => todo!("cannot handle nonstandard nodes"), - } - } + fn find_last_element(&self, node: &'b VNode<'b>) -> ElementId { + match node.dynamic_root(node.template.roots.len() - 1) { + None => node.root_ids.last().unwrap().get(), + Some(Text(t)) => t.id.get(), + Some(Fragment(t)) => self.find_last_element(t.last().unwrap()), + Some(Placeholder(t)) => t.get(), + Some(Component(comp)) => { + let scope = comp.scope.get().unwrap(); + match unsafe { self.scopes[scope.0].root_node().extend_lifetime_ref() } { + RenderReturn::Sync(Ok(t)) => self.find_last_element(t), + _ => todo!("cannot handle nonstandard nodes"), } } } } - fn replace_template(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) { - todo!("replacing should work!") + fn replace(&mut self, left: &'b VNode<'b>, right: &'b VNode<'b>) { + let first = self.find_first_element(left); + let id = self.replace_inner(left); + let created = self.create(right); + self.mutations.push(Mutation::ReplaceWith { + id: first, + m: created, + }); + self.try_reclaim(id); } - fn replace( - &mut self, - left_template: &'b VNode<'b>, - right_template: &'b VNode<'b>, - left: &'b DynamicNode<'b>, - right: &'b DynamicNode<'b>, - ) { - // Remove all but the first root - for root in &left_template.template.roots[1..] { - match root { - TemplateNode::Element { .. } => todo!(), - TemplateNode::Text(_) => todo!(), - TemplateNode::Dynamic(_) => todo!(), - TemplateNode::DynamicText(_) => todo!(), - } - } + fn replace_many(&mut self, left: &'b VNode<'b>, right: &'b [VNode<'b>]) { + let first = self.find_first_element(left); + let id = self.replace_inner(left); + let created = self.create_children(right); + self.mutations.push(Mutation::ReplaceWith { + id: first, + m: created, + }); + self.try_reclaim(id); } } @@ -836,7 +934,7 @@ fn matching_components<'a>( _ => return None, }; - (l.render_fn == r.render_fn).then(|| (l, r)) + Some((l, r)) }) .collect() } @@ -847,6 +945,7 @@ fn matching_components<'a>( /// - for text - we can use SetTextContent /// - for clearning children we can use RemoveChildren /// - for appending children we can use AppendChildren +#[allow(dead_code)] fn is_dyn_node_only_child(node: &VNode, idx: usize) -> bool { let path = node.template.node_paths[idx]; diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index c2a1947da..6f40c2484 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -1,29 +1,19 @@ -use std::{cell::RefCell, rc::Rc}; +use std::cell::RefCell; -use crate::{ScopeId, ScopeState}; +use crate::ScopeId; -pub struct ErrorContext { +/// A boundary that will capture any errors from child components +#[allow(dead_code)] +pub struct ErrorBoundary { error: RefCell>, + id: ScopeId, } -/// Catch all errors from the children and bubble them up to this component -/// -/// Returns the error and scope that caused the error -pub fn use_catch_error(cx: &ScopeState) -> Option<&(anyhow::Error, ScopeId)> { - let err_ctx = use_error_context(cx); - - let out = cx.use_hook(|| None); - - if let Some(error) = err_ctx.error.take() { - *out = Some(error); +impl ErrorBoundary { + pub fn new(id: ScopeId) -> Self { + Self { + error: RefCell::new(None), + id, + } } - - out.as_ref() -} - -/// Create a new error context at this component. -/// -/// This component will start to catch any errors that occur in its children. -pub fn use_error_context(cx: &ScopeState) -> &ErrorContext { - cx.use_hook(|| cx.provide_context(Rc::new(ErrorContext { error: None.into() }))) } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index cb14734d2..583f44c33 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -1,109 +1,110 @@ -use bumpalo::boxed::Box as BumpBox; use std::{ - any::Any, cell::{Cell, RefCell}, - fmt::Debug, rc::Rc, }; -pub struct UiEvent { - pub(crate) bubbles: Rc>, +/// A wrapper around some generic data that handles the event's state +/// +/// +/// Prevent this event from continuing to bubble up the tree to parent elements. +/// +/// # Example +/// +/// ```rust, ignore +/// rsx! { +/// button { +/// onclick: move |evt: Event| { +/// evt.cancel_bubble(); +/// +/// } +/// } +/// } +/// ``` +pub struct Event { pub(crate) data: Rc, + pub(crate) propogates: Rc>, } -impl UiEvent { - pub fn downcast(self) -> Option> { - Some(UiEvent { - bubbles: self.bubbles, - data: self.data.downcast().ok()?, - }) +impl Event { + /// Prevent this event from continuing to bubble up the tree to parent elements. + /// + /// # Example + /// + /// ```rust, ignore + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// evt.cancel_bubble(); + /// } + /// } + /// } + /// ``` + #[deprecated = "use stop_propogation instead"] + pub fn cancel_bubble(&self) { + self.propogates.set(false); + } + + /// Prevent this event from continuing to bubble up the tree to parent elements. + /// + /// # Example + /// + /// ```rust, ignore + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// evt.cancel_bubble(); + /// } + /// } + /// } + /// ``` + pub fn stop_propogation(&self) { + self.propogates.set(false); + } + + /// Get a reference to the inner data from this event + /// + /// ```rust, ignore + /// rsx! { + /// button { + /// onclick: move |evt: Event| { + /// let data = evt.inner.clone(); + /// cx.spawn(async move { + /// println!("{:?}", data); + /// }); + /// } + /// } + /// } + /// ``` + pub fn inner(&self) -> &Rc { + &self.data } } -impl Clone for UiEvent { + +impl Clone for Event { fn clone(&self) -> Self { Self { - bubbles: self.bubbles.clone(), + propogates: self.propogates.clone(), data: self.data.clone(), } } } -impl UiEvent { - pub fn cancel_bubble(&self) { - self.bubbles.set(false); - } -} -impl std::ops::Deref for UiEvent { +impl std::ops::Deref for Event { type Target = Rc; - fn deref(&self) -> &Self::Target { &self.data } } -impl std::fmt::Debug for UiEvent { +impl std::fmt::Debug for Event { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("UiEvent") - .field("bubble_state", &self.bubbles) + .field("bubble_state", &self.propogates) .field("data", &self.data) .finish() } } -/// Priority of Event Triggers. -/// -/// Internally, Dioxus will abort work that's taking too long if new, more important work arrives. Unlike React, Dioxus -/// won't be afraid to pause work or flush changes to the Real Dom. This is called "cooperative scheduling". Some Renderers -/// implement this form of scheduling internally, however Dioxus will perform its own scheduling as well. -/// -/// The ultimate goal of the scheduler is to manage latency of changes, prioritizing "flashier" changes over "subtler" changes. -/// -/// React has a 5-tier priority system. However, they break things into "Continuous" and "Discrete" priority. For now, -/// we keep it simple, and just use a 3-tier priority system. -/// -/// - `NoPriority` = 0 -/// - `LowPriority` = 1 -/// - `NormalPriority` = 2 -/// - `UserBlocking` = 3 -/// - `HighPriority` = 4 -/// - `ImmediatePriority` = 5 -/// -/// We still have a concept of discrete vs continuous though - discrete events won't be batched, but continuous events will. -/// This means that multiple "scroll" events will be processed in a single frame, but multiple "click" events will be -/// flushed before proceeding. Multiple discrete events is highly unlikely, though. -#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)] -pub enum EventPriority { - /// Work that must be completed during the EventHandler phase. - /// - /// Currently this is reserved for controlled inputs. - Immediate = 3, - - /// "High Priority" work will not interrupt other high priority work, but will interrupt medium and low priority work. - /// - /// This is typically reserved for things like user interaction. - /// - /// React calls these "discrete" events, but with an extra category of "user-blocking" (Immediate). - High = 2, - - /// "Medium priority" work is generated by page events not triggered by the user. These types of events are less important - /// than "High Priority" events and will take precedence over low priority events. - /// - /// This is typically reserved for VirtualEvents that are not related to keyboard or mouse input. - /// - /// React calls these "continuous" events (e.g. mouse move, mouse wheel, touch move, etc). - Medium = 1, - - /// "Low Priority" work will always be preempted unless the work is significantly delayed, in which case it will be - /// advanced to the front of the work queue until completed. - /// - /// The primary user of Low Priority work is the asynchronous work system (Suspense). - /// - /// This is considered "idle" work or "background" work. - Low = 0, -} - -type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; - /// The callback type generated by the `rsx!` macro when an `on` field is specified for components. /// /// This makes it possible to pass `move |evt| {}` style closures into components as property fields. @@ -112,9 +113,8 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; /// # Example /// /// ```rust, ignore -/// /// rsx!{ -/// MyComponent { onclick: move |evt| log::info!("clicked"), } +/// MyComponent { onclick: move |evt| log::info!("clicked") } /// } /// /// #[derive(Props)] @@ -132,21 +132,23 @@ type ExternalListenerCallback<'bump, T> = BumpBox<'bump, dyn FnMut(T) + 'bump>; /// /// ``` pub struct EventHandler<'bump, T = ()> { - /// The (optional) callback that the user specified - /// Uses a `RefCell` to allow for interior mutability, and FnMut closures. - pub callback: RefCell>>, + pub(super) callback: RefCell>>, } -impl<'a, T> Default for EventHandler<'a, T> { +impl Default for EventHandler<'_, T> { fn default() -> Self { Self { - callback: RefCell::new(None), + callback: Default::default(), } } } +type ExternalListenerCallback<'bump, T> = bumpalo::boxed::Box<'bump, dyn FnMut(T) + 'bump>; + impl EventHandler<'_, T> { /// Call this event handler with the appropriate event type + /// + /// This borrows the event using a RefCell. Recursively calling a listener will cause a panic. pub fn call(&self, event: T) { if let Some(callback) = self.callback.borrow_mut().as_mut() { callback(event); @@ -154,6 +156,8 @@ impl EventHandler<'_, T> { } /// Forcibly drop the internal handler callback, releasing memory + /// + /// This will force any future calls to "call" to not doing anything pub fn release(&self) { self.callback.replace(None); } diff --git a/packages/core/src/factory.rs b/packages/core/src/factory.rs deleted file mode 100644 index d92899f3d..000000000 --- a/packages/core/src/factory.rs +++ /dev/null @@ -1,289 +0,0 @@ -use std::{ - cell::{Cell, RefCell}, - fmt::Arguments, -}; - -use bumpalo::boxed::Box as BumpBox; -use bumpalo::Bump; -use std::future::Future; - -use crate::{ - any_props::{AnyProps, VProps}, - arena::ElementId, - innerlude::{DynamicNode, EventHandler, VComponent, VFragment, VText}, - Attribute, AttributeValue, Element, LazyNodes, Properties, Scope, ScopeState, VNode, -}; - -impl ScopeState { - /// Create some text that's allocated along with the other vnodes - pub fn text<'a>(&'a self, args: Arguments) -> DynamicNode<'a> { - let (text, _) = self.raw_text(args); - DynamicNode::Text(VText { - id: Cell::new(ElementId(0)), - value: text, - }) - } - - pub fn raw_text_inline<'a>(&'a self, args: Arguments) -> &'a str { - self.raw_text(args).0 - } - - pub fn raw_text<'a>(&'a self, args: Arguments) -> (&'a str, bool) { - match args.as_str() { - Some(static_str) => (static_str, true), - None => { - use bumpalo::core_alloc::fmt::Write; - let mut str_buf = bumpalo::collections::String::new_in(self.bump()); - str_buf.write_fmt(args).unwrap(); - (str_buf.into_bump_str(), false) - } - } - } - - pub fn fragment_from_iter<'a, 'c, I>( - &'a self, - node_iter: impl IntoDynNode<'a, I> + 'c, - ) -> DynamicNode { - node_iter.into_vnode(self) - } - - /// Create a new [`Attribute`] - pub fn attr<'a>( - &'a self, - name: &'static str, - value: impl IntoAttributeValue<'a>, - namespace: Option<&'static str>, - volatile: bool, - ) -> Attribute<'a> { - Attribute { - name, - namespace, - volatile, - value: value.into_value(self.bump()), - mounted_element: Cell::new(ElementId(0)), - } - } - - /// Create a new [`VNode::Component`] - pub fn component<'a, P, A, F: ComponentReturn<'a, A>>( - &'a self, - component: fn(Scope<'a, P>) -> F, - props: P, - fn_name: &'static str, - ) -> DynamicNode<'a> - where - P: Properties + 'a, - { - let as_component = component; - let vcomp = VProps::new(as_component, P::memoize, props); - let as_dyn: Box> = Box::new(vcomp); - let extended: Box = unsafe { std::mem::transmute(as_dyn) }; - - // let as_dyn: &dyn AnyProps = self.bump().alloc(vcomp); - // todo: clean up borrowed props - // if !P::IS_STATIC { - // let vcomp = &*vcomp; - // let vcomp = unsafe { std::mem::transmute(vcomp) }; - // self.scope.items.borrow_mut().borrowed_props.push(vcomp); - // } - - DynamicNode::Component(VComponent { - name: fn_name, - render_fn: component as *const (), - static_props: P::IS_STATIC, - props: Cell::new(Some(extended)), - scope: Cell::new(None), - }) - } - - /// Create a new [`EventHandler`] from an [`FnMut`] - pub fn event_handler<'a, T>(&'a self, f: impl FnMut(T) + 'a) -> EventHandler<'a, T> { - let handler: &mut dyn FnMut(T) = self.bump().alloc(f); - let caller = unsafe { BumpBox::from_raw(handler as *mut dyn FnMut(T)) }; - let callback = RefCell::new(Some(caller)); - EventHandler { callback } - } -} - -pub trait ComponentReturn<'a, A = ()> { - fn as_return(self, cx: &'a ScopeState) -> RenderReturn<'a>; -} -impl<'a> ComponentReturn<'a> for Element<'a> { - fn as_return(self, _cx: &ScopeState) -> RenderReturn<'a> { - RenderReturn::Sync(self) - } -} - -pub struct AsyncMarker; -impl<'a, F> ComponentReturn<'a, AsyncMarker> for F -where - F: Future> + 'a, -{ - fn as_return(self, cx: &'a ScopeState) -> RenderReturn<'a> { - let f: &mut dyn Future> = cx.bump().alloc(self); - let boxed = unsafe { BumpBox::from_raw(f) }; - let pined: BumpBox<_> = boxed.into(); - RenderReturn::Async(pined) - } -} - -pub enum RenderReturn<'a> { - /// A currently-available element - Sync(Element<'a>), - - /// An ongoing future that will resolve to a [`Element`] - Async(BumpBox<'a, dyn Future> + 'a>), -} - -impl<'a> RenderReturn<'a> { - pub(crate) unsafe fn extend_lifetime_ref<'c>(&self) -> &'c RenderReturn<'c> { - unsafe { std::mem::transmute(self) } - } - pub(crate) unsafe fn extend_lifetime<'c>(self) -> RenderReturn<'c> { - unsafe { std::mem::transmute(self) } - } -} - -pub trait IntoDynNode<'a, A = ()> { - fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a>; -} - -impl<'a, 'b> IntoDynNode<'a> for () { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { - DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0)))) - } -} -impl<'a, 'b> IntoDynNode<'a> for VNode<'a> { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { - DynamicNode::Fragment(VFragment::NonEmpty(_cx.bump().alloc([self]))) - } -} - -impl<'a, 'b, T: IntoDynNode<'a>> IntoDynNode<'a> for Option { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { - match self { - Some(val) => val.into_vnode(_cx), - None => DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0)))), - } - } -} - -impl<'a> IntoDynNode<'a> for &Element<'a> { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { - match self.as_ref() { - Ok(val) => val.clone().into_vnode(_cx), - _ => DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0)))), - } - } -} - -impl<'a, 'b> IntoDynNode<'a> for LazyNodes<'a, 'b> { - fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { - DynamicNode::Fragment(VFragment::NonEmpty(cx.bump().alloc([self.call(cx)]))) - } -} - -impl<'b> IntoDynNode<'_> for &'b str { - fn into_vnode(self, cx: &ScopeState) -> DynamicNode { - cx.text(format_args!("{}", self)) - } -} - -impl IntoDynNode<'_> for String { - fn into_vnode(self, cx: &ScopeState) -> DynamicNode { - cx.text(format_args!("{}", self)) - } -} - -impl<'b> IntoDynNode<'b> for Arguments<'_> { - fn into_vnode(self, cx: &'b ScopeState) -> DynamicNode<'b> { - cx.text(self) - } -} - -impl<'a, 'b> IntoDynNode<'a> for &VNode<'a> { - fn into_vnode(self, _cx: &'a ScopeState) -> DynamicNode<'a> { - todo!() - // VNode { - // node_id: self.node_id.clone(), - // parent: self.parent, - // template: self.template, - // root_ids: self.root_ids, - // key: self.key, - // dynamic_nodes: self.dynamic_nodes, - // dynamic_attrs: self.dynamic_attrs, - // } - } -} - -pub trait IntoTemplate<'a> { - fn into_template(self, _cx: &'a ScopeState) -> VNode<'a>; -} -impl<'a, 'b> IntoTemplate<'a> for VNode<'a> { - fn into_template(self, _cx: &'a ScopeState) -> VNode<'a> { - self - } -} -impl<'a, 'b> IntoTemplate<'a> for LazyNodes<'a, 'b> { - fn into_template(self, cx: &'a ScopeState) -> VNode<'a> { - self.call(cx) - } -} - -// Note that we're using the E as a generic but this is never crafted anyways. -pub struct FromNodeIterator; -impl<'a, T, I> IntoDynNode<'a, FromNodeIterator> for T -where - T: Iterator, - I: IntoTemplate<'a>, -{ - fn into_vnode(self, cx: &'a ScopeState) -> DynamicNode<'a> { - let mut nodes = bumpalo::collections::Vec::new_in(cx.bump()); - - for node in self { - nodes.push(node.into_template(cx)); - } - - let children = nodes.into_bump_slice(); - - match children.len() { - 0 => DynamicNode::Fragment(VFragment::Empty(Cell::new(ElementId(0)))), - _ => DynamicNode::Fragment(VFragment::NonEmpty(children)), - } - } -} - -/// A value that can be converted into an attribute value -pub trait IntoAttributeValue<'a> { - /// Convert into an attribute value - fn into_value(self, bump: &'a Bump) -> AttributeValue<'a>; -} - -impl<'a> IntoAttributeValue<'a> for &'a str { - fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { - AttributeValue::Text(self) - } -} -impl<'a> IntoAttributeValue<'a> for f32 { - fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { - AttributeValue::Float(self) - } -} -impl<'a> IntoAttributeValue<'a> for i32 { - fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { - AttributeValue::Int(self) - } -} -impl<'a> IntoAttributeValue<'a> for bool { - fn into_value(self, _: &'a Bump) -> AttributeValue<'a> { - AttributeValue::Bool(self) - } -} -impl<'a> IntoAttributeValue<'a> for Arguments<'_> { - fn into_value(self, bump: &'a Bump) -> AttributeValue<'a> { - use bumpalo::core_alloc::fmt::Write; - let mut str_buf = bumpalo::collections::String::new_in(bump); - str_buf.write_fmt(self).unwrap(); - AttributeValue::Text(str_buf.into_bump_str()) - } -} diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs index f2e539d17..d5dfd3c50 100644 --- a/packages/core/src/fragment.rs +++ b/packages/core/src/fragment.rs @@ -27,9 +27,8 @@ use crate::innerlude::*; /// You want to use this free-function when your fragment needs a key and simply returning multiple nodes from rsx! won't cut it. #[allow(non_upper_case_globals, non_snake_case)] pub fn Fragment<'a>(cx: Scope<'a, FragmentProps<'a>>) -> Element { - let children = cx.props.0.as_ref().unwrap(); + let children = cx.props.0.as_ref().map_err(|e| anyhow::anyhow!("{}", e))?; Ok(VNode { - node_id: children.node_id.clone(), key: children.key.clone(), parent: children.parent.clone(), template: children.template, diff --git a/packages/core/src/future_container.rs b/packages/core/src/future_container.rs deleted file mode 100644 index 0d2f5b549..000000000 --- a/packages/core/src/future_container.rs +++ /dev/null @@ -1,63 +0,0 @@ -use futures_channel::mpsc::UnboundedSender; -use slab::Slab; -use std::future::Future; -use std::{cell::RefCell, rc::Rc, sync::Arc}; - -use crate::innerlude::ScopeId; -/// The type of message that can be sent to the scheduler. -/// -/// These messages control how the scheduler will process updates to the UI. -#[derive(Debug)] -pub enum SchedulerMsg { - /// Events from athe Renderer - Event, - - /// Immediate updates from Components that mark them as dirty - Immediate(ScopeId), - - /// Mark all components as dirty and update them - DirtyAll, - - /// New tasks from components that should be polled when the next poll is ready - NewTask(ScopeId), -} - -// todo extract this so spawning doesn't require refcell and rc doesnt need to be tracked -#[derive(Clone)] -pub struct FutureQueue { - pub sender: UnboundedSender, - pub queue: RefCell>>>, -} - -impl FutureQueue { - pub fn new(sender: UnboundedSender) -> Self { - Self { - sender, - queue: Default::default(), - } - } - - pub fn spawn(&self, scope: ScopeId, fut: impl Future + 'static) -> TaskId { - let id = self.queue.borrow_mut().insert(Arc::new(fut)); - - TaskId { id, scope } - } - - pub fn remove(&self, id: TaskId) { - todo!() - } -} - -/// A task's unique identifier. -/// -/// `TaskId` is a `usize` that is unique across the entire [`VirtualDom`] and across time. [`TaskID`]s will never be reused -/// once a Task has been completed. -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)] -pub struct TaskId { - /// The global ID of the task - pub id: usize, - - /// The original scope that this task was scheduled in - pub scope: ScopeId, -} diff --git a/packages/core/src/garbage.rs b/packages/core/src/garbage.rs deleted file mode 100644 index 056b14481..000000000 --- a/packages/core/src/garbage.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::{nodes::VNode, scopes::ScopeId, virtual_dom::VirtualDom, DynamicNode, Mutations}; - -impl<'b> VirtualDom { - pub fn drop_scope(&mut self, id: ScopeId) { - // let scope = self.scopes.get(id.0).unwrap(); - - // let root = scope.root_node(); - // let root = unsafe { std::mem::transmute(root) }; - - // self.drop_template(root, false); - todo!() - } - - pub fn drop_template( - &mut self, - mutations: &mut Mutations, - template: &'b VNode<'b>, - gen_roots: bool, - ) { - // for node in template.dynamic_nodes.iter() { - // match node { - // DynamicNode::Text { id, .. } => {} - - // DynamicNode::Component { .. } => { - // todo!() - // } - - // DynamicNode::Fragment { inner, nodes } => {} - // DynamicNode::Placeholder(_) => todo!(), - // _ => todo!(), - // } - // } - } -} diff --git a/packages/core/src/lazynodes.rs b/packages/core/src/lazynodes.rs index 76c25641f..020b307d9 100644 --- a/packages/core/src/lazynodes.rs +++ b/packages/core/src/lazynodes.rs @@ -3,7 +3,7 @@ //! This module provides support for a type called `LazyNodes` which is a micro-heap located on the stack to make calls //! to `rsx!` more efficient. //! -//! To support returning rsx! from branches in match statements, we need to use dynamic dispatch on [`NodeFactory`] closures. +//! To support returning rsx! from branches in match statements, we need to use dynamic dispatch on [`ScopeState`] closures. //! //! This can be done either through boxing directly, or by using dynamic-sized-types and a custom allocator. In our case, //! we build a tiny alloactor in the stack and allocate the closure into that. @@ -17,7 +17,7 @@ use std::mem; /// A concrete type provider for closures that build [`VNode`] structures. /// /// This struct wraps lazy structs that build [`VNode`] trees Normally, we cannot perform a blanket implementation over -/// closures, but if we wrap the closure in a concrete type, we can maintain separate implementations of [`IntoVNode`]. +/// closures, but if we wrap the closure in a concrete type, we can use it for different branches in matching. /// /// /// ```rust, ignore @@ -27,13 +27,11 @@ pub struct LazyNodes<'a, 'b> { inner: StackNodeStorage<'a, 'b>, } -pub type NodeFactory<'a> = &'a ScopeState; - type StackHeapSize = [usize; 16]; enum StackNodeStorage<'a, 'b> { Stack(LazyStack), - Heap(Box>) -> Option> + 'b>), + Heap(Box) -> Option> + 'b>), } impl<'a, 'b> LazyNodes<'a, 'b> { @@ -42,11 +40,11 @@ impl<'a, 'b> LazyNodes<'a, 'b> { /// If the closure cannot fit into the stack allocation (16 bytes), then it /// is placed on the heap. Most closures will fit into the stack, and is /// the most optimal way to use the creation function. - pub fn new(val: impl FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b) -> Self { + pub fn new(val: impl FnOnce(&'a ScopeState) -> VNode<'a> + 'b) -> Self { // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch let mut slot = Some(val); - let val = move |fac: Option>| { + let val = move |fac: Option<&'a ScopeState>| { fac.map( slot.take() .expect("LazyNodes closure to be called only once"), @@ -67,13 +65,13 @@ impl<'a, 'b> LazyNodes<'a, 'b> { /// Create a new [`LazyNodes`] closure, but force it onto the heap. pub fn new_boxed(inner: F) -> Self where - F: FnOnce(NodeFactory<'a>) -> VNode<'a> + 'b, + F: FnOnce(&'a ScopeState) -> VNode<'a> + 'b, { // there's no way to call FnOnce without a box, so we need to store it in a slot and use static dispatch let mut slot = Some(inner); Self { - inner: StackNodeStorage::Heap(Box::new(move |fac: Option>| { + inner: StackNodeStorage::Heap(Box::new(move |fac: Option<&'a ScopeState>| { fac.map( slot.take() .expect("LazyNodes closure to be called only once"), @@ -84,9 +82,9 @@ impl<'a, 'b> LazyNodes<'a, 'b> { unsafe fn new_inner(val: F) -> Self where - F: FnMut(Option>) -> Option> + 'b, + F: FnMut(Option<&'a ScopeState>) -> Option> + 'b, { - let mut ptr: *const _ = &val as &dyn FnMut(Option>) -> Option>; + let mut ptr: *const _ = &val as &dyn FnMut(Option<&'a ScopeState>) -> Option>; assert_eq!( ptr as *const u8, &val as *const _ as *const u8, @@ -162,12 +160,10 @@ impl<'a, 'b> LazyNodes<'a, 'b> { /// ```rust, ignore /// let f = LazyNodes::new(move |f| f.element("div", [], [], [] None)); /// - /// let fac = NodeFactory::new(&cx); - /// /// let node = f.call(cac); /// ``` #[must_use] - pub fn call(self, f: NodeFactory<'a>) -> VNode<'a> { + pub fn call(self, f: &'a ScopeState) -> VNode<'a> { match self.inner { StackNodeStorage::Heap(mut lazy) => { lazy(Some(f)).expect("Closure should not be called twice") @@ -184,18 +180,18 @@ struct LazyStack { } impl LazyStack { - fn call<'a>(&mut self, f: NodeFactory<'a>) -> VNode<'a> { + fn call<'a>(&mut self, f: &'a ScopeState) -> VNode<'a> { let LazyStack { buf, .. } = self; let data = buf.as_ref(); let info_size = - mem::size_of::<*mut dyn FnMut(Option>) -> Option>>() + mem::size_of::<*mut dyn FnMut(Option<&'a ScopeState>) -> Option>>() / mem::size_of::() - 1; let info_ofs = data.len() - info_size; - let g: *mut dyn FnMut(Option>) -> Option> = + let g: *mut dyn FnMut(Option<&'a ScopeState>) -> Option> = unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; self.dropped = true; @@ -210,14 +206,14 @@ impl Drop for LazyStack { let LazyStack { buf, .. } = self; let data = buf.as_ref(); - let info_size = mem::size_of::< - *mut dyn FnMut(Option>) -> Option>, - >() / mem::size_of::() - - 1; + let info_size = + mem::size_of::<*mut dyn FnMut(Option<&ScopeState>) -> Option>>() + / mem::size_of::() + - 1; let info_ofs = data.len() - info_size; - let g: *mut dyn FnMut(Option>) -> Option> = + let g: *mut dyn FnMut(Option<&ScopeState>) -> Option> = unsafe { make_fat_ptr(data[..].as_ptr() as usize, &data[info_ofs..]) }; self.dropped = true; @@ -252,73 +248,3 @@ unsafe fn make_fat_ptr(data_ptr: usize, meta_vals: &[usize]) -> *mut fn round_to_words(len: usize) -> usize { (len + mem::size_of::() - 1) / mem::size_of::() } - -// #[cfg(test)] -// mod tests { -// use super::*; -// use crate::innerlude::{Element, Scope, VirtualDom}; - -// #[test] -// fn it_works() { -// fn app(cx: Scope<()>) -> Element { -// cx.render(LazyNodes::new(|f| f.text(format_args!("hello world!")))) -// } - -// let mut dom = VirtualDom::new(app); -// dom.rebuild(); - -// let g = dom.base_scope().root_node(); -// dbg!(g); -// } - -// #[test] -// fn it_drops() { -// use std::rc::Rc; - -// struct AppProps { -// inner: Rc, -// } - -// fn app(cx: Scope) -> Element { -// struct DropInner { -// id: i32, -// } -// impl Drop for DropInner { -// fn drop(&mut self) { -// eprintln!("dropping inner"); -// } -// } - -// let caller = { -// let it = (0..10).map(|i| { -// let val = cx.props.inner.clone(); -// LazyNodes::new(move |f| { -// eprintln!("hell closure"); -// let inner = DropInner { id: i }; -// f.text(format_args!("hello world {:?}, {:?}", inner.id, val)) -// }) -// }); - -// LazyNodes::new(|f| { -// eprintln!("main closure"); -// f.fragment_from_iter(it) -// }) -// }; - -// cx.render(caller) -// } - -// let inner = Rc::new(0); -// let mut dom = VirtualDom::new_with_props( -// app, -// AppProps { -// inner: inner.clone(), -// }, -// ); -// dom.rebuild(); - -// drop(dom); - -// assert_eq!(Rc::strong_count(&inner), 1); -// } -// } diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index d88e73986..613a4a115 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -1,3 +1,6 @@ +#![doc = include_str!("../README.md")] +#![warn(missing_docs)] + mod any_props; mod arena; mod bump_frame; @@ -6,9 +9,7 @@ mod diff; mod dirty_scope; mod error_boundary; mod events; -mod factory; mod fragment; -mod garbage; mod lazynodes; mod mutations; mod nodes; @@ -21,10 +22,12 @@ mod virtual_dom; pub(crate) mod innerlude { pub use crate::arena::*; pub use crate::dirty_scope::*; + pub use crate::error_boundary::*; pub use crate::events::*; pub use crate::fragment::*; pub use crate::lazynodes::*; pub use crate::mutations::*; + pub use crate::nodes::RenderReturn; pub use crate::nodes::*; pub use crate::properties::*; pub use crate::scheduler::*; @@ -64,45 +67,13 @@ pub(crate) mod innerlude { /// ) /// ``` pub type Component

= fn(Scope

) -> Element; - - /// A list of attributes - pub type Attributes<'a> = Option<&'a [Attribute<'a>]>; } pub use crate::innerlude::{ - // AnyAttributeValue, AnyEvent, - fc_to_builder, - Attribute, - AttributeValue, - Attributes, - Component, - DynamicNode, - Element, - ElementId, - ElementRef, - EventPriority, - Fragment, - LazyNodes, - Mutation, - Mutations, - NodeFactory, - Properties, - Scope, - ScopeId, - ScopeState, - Scoped, - SuspenseBoundary, - SuspenseContext, - TaskId, - Template, - TemplateAttribute, - TemplateNode, - UiEvent, - VComponent, - VFragment, - VNode, - VText, - VirtualDom, + fc_to_builder, Attribute, AttributeValue, Component, DynamicNode, Element, ElementId, Event, + Fragment, IntoDynNode, LazyNodes, Mutation, Mutations, Properties, RenderReturn, Scope, + ScopeId, ScopeState, Scoped, SuspenseContext, TaskId, Template, TemplateAttribute, + TemplateNode, VComponent, VNode, VText, VirtualDom, }; /// The purpose of this module is to alleviate imports of many common types @@ -110,9 +81,9 @@ pub use crate::innerlude::{ /// This includes types like [`Scope`], [`Element`], and [`Component`]. pub mod prelude { pub use crate::innerlude::{ - fc_to_builder, Element, EventHandler, EventPriority, Fragment, LazyNodes, NodeFactory, - Properties, Scope, ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, - TemplateNode, UiEvent, VNode, VirtualDom, + fc_to_builder, Element, Event, EventHandler, Fragment, LazyNodes, Properties, Scope, + ScopeId, ScopeState, Scoped, TaskId, Template, TemplateAttribute, TemplateNode, VNode, + VirtualDom, }; } @@ -120,28 +91,4 @@ pub mod exports { //! Important dependencies that are used by the rest of the library //! Feel free to just add the dependencies in your own Crates.toml pub use bumpalo; - pub use futures_channel; -} - -#[macro_export] -/// A helper macro for using hooks in async environements. -/// -/// # Usage -/// -/// -/// ```ignore -/// let (data) = use_ref(&cx, || {}); -/// -/// let handle_thing = move |_| { -/// to_owned![data] -/// cx.spawn(async move { -/// // do stuff -/// }); -/// } -/// ``` -macro_rules! to_owned { - ($($es:ident),+) => {$( - #[allow(unused_mut)] - let mut $es = $es.to_owned(); - )*} } diff --git a/packages/core/src/mutations.rs b/packages/core/src/mutations.rs index 3627836df..3687cd956 100644 --- a/packages/core/src/mutations.rs +++ b/packages/core/src/mutations.rs @@ -1,59 +1,60 @@ -use crate::{arena::ElementId, ScopeId}; +use fxhash::FxHashSet; -#[derive(Debug)] +use crate::{arena::ElementId, ScopeId, Template}; + +/// A container for all the relevant steps to modify the Real DOM +/// +/// This object provides a bunch of important information for a renderer to use patch the Real Dom with the state of the +/// VirtualDom. This includes the scopes that were modified, the templates that were discovered, and a list of changes +/// in the form of a [`Mutation`]. +/// +/// These changes are specific to one subtree, so to patch multiple subtrees, you'd need to handle each set separately. +/// +/// Templates, however, apply to all subtrees, not just target subtree. +/// +/// Mutations are the only link between the RealDOM and the VirtualDOM. +#[derive(Debug, Default)] +#[must_use = "not handling edits can lead to visual inconsistencies in UI"] pub struct Mutations<'a> { + /// The ID of the subtree that these edits are targetting pub subtree: usize, - pub template_mutations: Vec>, + + /// The list of Scopes that were diffed, created, and removed during the Diff process. + pub dirty_scopes: FxHashSet, + + /// Any templates encountered while diffing the DOM. + /// + /// These must be loaded into a cache before applying the edits + pub templates: Vec>, + + /// Any mutations required to patch the renderer to match the layout of the VirtualDom pub edits: Vec>, } impl<'a> Mutations<'a> { - pub fn new() -> Self { - Self { - subtree: 0, - edits: Vec::new(), - template_mutations: Vec::new(), - } - } - - /// A useful tool for testing mutations - /// /// Rewrites IDs to just be "template", so you can compare the mutations + /// + /// Used really only for testing pub fn santize(mut self) -> Self { - for edit in self - .template_mutations - .iter_mut() - .chain(self.edits.iter_mut()) - { - match edit { - Mutation::LoadTemplate { name, .. } => *name = "template", - Mutation::SaveTemplate { name, .. } => *name = "template", - _ => {} + for edit in self.edits.iter_mut() { + if let Mutation::LoadTemplate { name, .. } = edit { + *name = "template" } } self } -} -impl<'a> std::ops::Deref for Mutations<'a> { - type Target = Vec>; - - fn deref(&self) -> &Self::Target { - &self.edits + /// Push a new mutation into the dom_edits list + pub(crate) fn push(&mut self, mutation: Mutation<'static>) { + self.edits.push(mutation) } } -impl std::ops::DerefMut for Mutations<'_> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.edits - } -} - -/* -each subtree has its own numbering scheme -*/ - +/// A `Mutation` represents a single instruction for the renderer to use to modify the UI tree to match the state +/// of the Dioxus VirtualDom. +/// +/// These edits can be serialized and sent over the network or through any interface #[cfg_attr( feature = "serialize", derive(serde::Serialize, serde::Deserialize), @@ -61,52 +62,111 @@ each subtree has its own numbering scheme )] #[derive(Debug, PartialEq, Eq)] pub enum Mutation<'a> { + /// Add these m children to the target element AppendChildren { + /// The ID of the element being mounted to + id: ElementId, + + /// The number of nodes on the stack m: usize, }, + /// Assign the element at the given path the target ElementId. + /// + /// The path is in the form of a list of indices based on children. Templates cannot have more than 255 children per + /// element, hence the use of a single byte. + /// + /// AssignId { + /// The path of the child of the topmost node on the stack + /// + /// A path of `[]` represents the topmost node. A path of `[0]` represents the first child. + /// `[0,1,2]` represents 1st child's 2nd child's 3rd child. path: &'static [u8], + + /// The ID we're assigning to this element/placeholder. + /// + /// This will be used later to modify the element or replace it with another element. id: ElementId, }, - CreateElement { - name: &'a str, - }, - CreateElementNamespace { - name: &'a str, - namespace: &'a str, - }, + /// Create an placeholder int he DOM that we will use later. + /// + /// Dioxus currently requires the use of placeholders to maintain a re-entrance point for things like list diffing CreatePlaceholder { - id: ElementId, - }, - CreateStaticPlaceholder, - CreateStaticText { - value: &'a str, - }, - CreateTextNode { - value: &'a str, - id: ElementId, - }, - HydrateText { - path: &'static [u8], - value: &'a str, - id: ElementId, - }, - LoadTemplate { - name: &'static str, - index: usize, + /// The ID we're assigning to this element/placeholder. + /// + /// This will be used later to modify the element or replace it with another element. id: ElementId, }, - // Take the current element and replace it with the element with the given id. - ReplaceWith { + /// Create a node specifically for text with the given value + CreateTextNode { + /// The text content of this text node + value: &'a str, + + /// The ID we're assigning to this specific text nodes + /// + /// This will be used later to modify the element or replace it with another element. id: ElementId, + }, + + /// Hydrate an existing text node at the given path with the given text. + /// + /// Assign this text node the given ID since we will likely need to modify this text at a later point + HydrateText { + /// The path of the child of the topmost node on the stack + /// + /// A path of `[]` represents the topmost node. A path of `[0]` represents the first child. + /// `[0,1,2]` represents 1st child's 2nd child's 3rd child. + path: &'static [u8], + + /// The value of the textnode that we want to set the placeholder with + value: &'a str, + + /// The ID we're assigning to this specific text nodes + /// + /// This will be used later to modify the element or replace it with another element. + id: ElementId, + }, + + /// Load and clone an existing node from a template saved under that specific name + /// + /// Dioxus guarantees that the renderer will have already been provided the template. + /// When the template is picked up in the template list, it should be saved under its "name" - here, the name + LoadTemplate { + /// The "name" of the template. When paired with `rsx!`, this is autogenerated + name: &'static str, + + /// Which root are we loading from the template? + /// + /// The template is stored as a list of nodes. This index represents the position of that root + index: usize, + + /// The ID we're assigning to this element being loaded from the template + /// + /// This will be used later to move the element around in lists + id: ElementId, + }, + + /// Replace the target element (given by its ID) with the topmost m nodes on the stack + ReplaceWith { + /// The ID of the node we're going to replace with + id: ElementId, + + /// The number of nodes on the stack to use to replace m: usize, }, + /// Replace an existing element in the template at the given path with the m nodes on the stack ReplacePlaceholder { + /// The path of the child of the topmost node on the stack + /// + /// A path of `[]` represents the topmost node. A path of `[0]` represents the first child. + /// `[0,1,2]` represents 1st child's 2nd child's 3rd child. path: &'static [u8], + + /// The number of nodes on the stack to use to replace m: usize, }, @@ -128,47 +188,46 @@ pub enum Mutation<'a> { m: usize, }, - SaveTemplate { - name: &'static str, - m: usize, - }, - + /// Set the value of a node's attribute. SetAttribute { + /// The name of the attribute to set. name: &'a str, + /// The value of the attribute. value: &'a str, + + /// The ID of the node to set the attribute of. id: ElementId, - // value: &'bump str, /// The (optional) namespace of the attribute. /// For instance, "style" is in the "style" namespace. ns: Option<&'a str>, }, - SetStaticAttribute { - name: &'a str, - value: &'a str, - ns: Option<&'a str>, - }, - + /// Set the value of a node's attribute. SetBoolAttribute { + /// The name of the attribute to set. name: &'a str, + + /// The value of the attribute. value: bool, + + /// The ID of the node to set the attribute of. id: ElementId, }, - SetInnerText { - value: &'a str, - }, - + /// Set the textcontent of a node. SetText { + /// The textcontent of the node value: &'a str, + + /// The ID of the node to set the textcontent of. id: ElementId, }, /// Create a new Event Listener. NewEventListener { /// The name of the event to listen for. - event_name: &'a str, + name: &'a str, /// The ID of the node to attach the listener to. scope: ScopeId, @@ -179,13 +238,22 @@ pub enum Mutation<'a> { /// Remove an existing Event Listener. RemoveEventListener { + /// The name of the event to remove. + name: &'a str, + /// The ID of the node to remove. id: ElementId, - - /// The name of the event to remove. - event: &'a str, }, + + /// Remove a particular node from the DOM Remove { + /// The ID of the node to remove. + id: ElementId, + }, + + /// Push the given root node onto our stack. + PushRoot { + /// The ID of the root node to push. id: ElementId, }, } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index 1719388ad..27eed58d4 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,20 +1,38 @@ -use crate::{any_props::AnyProps, arena::ElementId, Element, ScopeId, ScopeState, UiEvent}; +use crate::{ + any_props::AnyProps, arena::ElementId, Element, Event, LazyNodes, ScopeId, ScopeState, +}; +use bumpalo::boxed::Box as BumpBox; +use bumpalo::Bump; use std::{ any::{Any, TypeId}, cell::{Cell, RefCell}, + fmt::Arguments, + future::Future, }; pub type TemplateId = &'static str; +/// The actual state of the component's most recent computation +/// +/// Because Dioxus accepts components in the form of `async fn(Scope) -> Result`, we need to support both +/// sync and async versions. +/// +/// Dioxus will do its best to immediately resolve any async components into a regular Element, but as an implementor +/// you might need to handle the case where there's no node immediately ready. +pub enum RenderReturn<'a> { + /// A currently-available element + Sync(Element<'a>), + + /// An ongoing future that will resolve to a [`Element`] + Async(BumpBox<'a, dyn Future> + 'a>), +} + /// A reference to a template along with any context needed to hydrate it /// /// The dynamic parts of the template are stored separately from the static parts. This allows faster diffing by skipping /// static parts of the template. #[derive(Debug, Clone)] pub struct VNode<'a> { - // The ID assigned for the root of this template - pub node_id: Cell, - /// The key given to the root of this template. /// /// In fragments, this is the key of the first child. In other cases, it is the key of the root. @@ -38,66 +56,170 @@ pub struct VNode<'a> { } impl<'a> VNode<'a> { + /// Create a template with no nodes that will be skipped over during diffing pub fn empty() -> Element<'a> { Ok(VNode { - node_id: Cell::new(ElementId(0)), key: None, parent: None, root_ids: &[], dynamic_nodes: &[], dynamic_attrs: &[], template: Template { - id: "dioxus-empty", + name: "dioxus-empty", roots: &[], node_paths: &[], attr_paths: &[], }, }) } -} -#[derive(Debug, Clone, Copy)] -pub struct Template<'a> { - pub id: &'a str, - pub roots: &'a [TemplateNode<'a>], - pub node_paths: &'a [&'a [u8]], - pub attr_paths: &'a [&'a [u8]], -} - -/// A weird-ish variant of VNodes with way more limited types -#[derive(Debug, Clone, Copy)] -pub enum TemplateNode<'a> { - Element { - tag: &'a str, - namespace: Option<&'a str>, - attrs: &'a [TemplateAttribute<'a>], - children: &'a [TemplateNode<'a>], - inner_opt: bool, - }, - Text(&'a str), - Dynamic(usize), - DynamicText(usize), -} - -#[derive(Debug)] -pub enum DynamicNode<'a> { - Component(VComponent<'a>), - Text(VText<'a>), - Fragment(VFragment<'a>), -} - -impl<'a> DynamicNode<'a> { - pub fn is_component(&self) -> bool { - matches!(self, DynamicNode::Component(_)) + /// Load a dynamic root at the given index + /// + /// Returns [`None`] if the root is actually a static node (Element/Text) + pub fn dynamic_root(&self, idx: usize) -> Option<&'a DynamicNode<'a>> { + match &self.template.roots[idx] { + TemplateNode::Element { .. } | TemplateNode::Text(_) => None, + TemplateNode::Dynamic(id) | TemplateNode::DynamicText(id) => { + Some(&self.dynamic_nodes[*id]) + } + } } } +/// A static layout of a UI tree that describes a set of dynamic and static nodes. +/// +/// This is the core innovation in Dioxus. Most UIs are made of static nodes, yet participate in diffing like any +/// dynamic node. This struct can be created at compile time. It promises that its name is unique, allow Dioxus to use +/// its static description of the UI to skip immediately to the dynamic nodes during diffing. +/// +/// For this to work properly, the [`Template::name`] *must* be unique across your entire project. This can be done via variety of +/// ways, with the suggested approach being the unique code location (file, line, col, etc). +#[cfg_attr(feature = "serialize", derive(serde::Serialize))] +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] +pub struct Template<'a> { + /// The name of the template. This must be unique across your entire program for template diffing to work properly + /// + /// If two templates have the same name, it's likely that Dioxus will panic when diffing. + pub name: &'a str, + + /// The list of template nodes that make up the template + /// + /// Unlike react, calls to `rsx!` can have multiple roots. This list supports that paradigm. + pub roots: &'a [TemplateNode<'a>], + + /// The paths of each node relative to the root of the template. + /// + /// These will be one segment shorter than the path sent to the renderer since those paths are relative to the + /// topmost element, not the `roots` field. + pub node_paths: &'a [&'a [u8]], + + /// The paths of each dynamic attribute relative to the root of the template + /// + /// These will be one segment shorter than the path sent to the renderer since those paths are relative to the + /// topmost element, not the `roots` field. + pub attr_paths: &'a [&'a [u8]], +} + +/// A statically known node in a layout. +/// +/// This can be created at compile time, saving the VirtualDom time when diffing the tree +#[derive(Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize))] +pub enum TemplateNode<'a> { + /// An statically known element in the dom. + /// + /// In HTML this would be something like `

` + Element { + /// The name of the element + /// + /// IE for a div, it would be the string "div" + tag: &'a str, + + /// The namespace of the element + /// + /// In HTML, this would be a valid URI that defines a namespace for all elements below it + /// SVG is an example of this namespace + namespace: Option<&'a str>, + + /// A list of possibly dynamic attribues for this element + /// + /// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"`. + attrs: &'a [TemplateAttribute<'a>], + + /// A list of template nodes that define another set of template nodes + children: &'a [TemplateNode<'a>], + }, + + /// This template node is just a piece of static text + Text(&'a str), + + /// This template node is unknown, and needs to be created at runtime. + Dynamic(usize), + + /// This template node is known to be some text, but needs to be created at runtime + /// + /// This is separate from the pure Dynamic variant for various optimizations + DynamicText(usize), +} + +/// A node created at runtime +/// +/// This node's index in the DynamicNode list on VNode should match its repsective `Dynamic` index +#[derive(Debug)] +pub enum DynamicNode<'a> { + /// A component node + /// + /// Most of the time, Dioxus will actually know which component this is as compile time, but the props and + /// assigned scope are dynamic. + /// + /// The actual VComponent can be dynamic between two VNodes, though, allowing implementations to swap + /// the render function at runtime + Component(VComponent<'a>), + + /// A text node + Text(VText<'a>), + + /// A placeholder + /// + /// Used by suspense when a node isn't ready and by fragments that don't render anything + /// + /// In code, this is just an ElementId whose initial value is set to 0 upon creation + Placeholder(Cell), + + /// A list of VNodes. + /// + /// Note that this is not a list of dynamic nodes. These must be VNodes and created through conditional rendering + /// or iterators. + Fragment(&'a [VNode<'a>]), +} + +impl Default for DynamicNode<'_> { + fn default() -> Self { + Self::Placeholder(Default::default()) + } +} + +/// An instance of a child component pub struct VComponent<'a> { + /// The name of this component pub name: &'static str, + + /// Are the props valid for the 'static lifetime? + /// + /// Internally, this is used as a guarantee. Externally, this might be incorrect, so don't count on it. + /// + /// This flag is assumed by the [`crate::Properties`] trait which is unsafe to implement pub static_props: bool, + + /// The assigned Scope for this component pub scope: Cell>, - pub props: Cell + 'a>>>, + + /// The function pointer of the component, known at compile time + /// + /// It is possible that components get folded at comppile time, so these shouldn't be really used as a key pub render_fn: *const (), + + pub(crate) props: Cell + 'a>>>, } impl<'a> std::fmt::Debug for VComponent<'a> { @@ -110,65 +232,91 @@ impl<'a> std::fmt::Debug for VComponent<'a> { } } +/// An instance of some text, mounted to the DOM #[derive(Debug)] pub struct VText<'a> { - pub id: Cell, + /// The actual text itself pub value: &'a str, + + /// The ID of this node in the real DOM + pub id: Cell, } -#[derive(Debug)] -pub enum VFragment<'a> { - Empty(Cell), - NonEmpty(&'a [VNode<'a>]), -} - -#[derive(Debug)] +/// An attribute of the TemplateNode, created at compile time +#[derive(Debug, PartialEq, Hash, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub enum TemplateAttribute<'a> { + /// This attribute is entirely known at compile time, enabling Static { - name: &'static str, + /// The name of this attribute. + /// + /// For example, the `href` attribute in `href="https://example.com"`, would have the name "href" + name: &'a str, + + /// The value of this attribute, known at compile time + /// + /// Currently this only accepts &str, so values, even if they're known at compile time, are not known value: &'a str, - namespace: Option<&'static str>, - volatile: bool, + + /// The namespace of this attribute. Does not exist in the HTML spec + namespace: Option<&'a str>, }, + + /// The attribute in this position is actually determined dynamically at runtime + /// + /// This is the index into the dynamic_attributes field on the container VNode Dynamic(usize), } +/// An attribute on a DOM node, such as `id="my-thing"` or `href="https://example.com"` #[derive(Debug)] pub struct Attribute<'a> { + /// The name of the attribute. pub name: &'a str, + + /// The value of the attribute pub value: AttributeValue<'a>, + + /// The namespace of the attribute. + /// + /// Doesn’t exist in the html spec. Used in Dioxus to denote “style” tags and other attribute groups. pub namespace: Option<&'static str>, + + /// The element in the DOM that this attribute belongs to pub mounted_element: Cell, + + /// An indication of we should always try and set the attribute. Used in controlled components to ensure changes are propagated pub volatile: bool, } +/// Any of the built-in values that the Dioxus VirtualDom supports as dynamic attributes on elements +/// +/// These are built-in to be faster during the diffing process. To use a custom value, use the [`AttributeValue::Any`] +/// variant. pub enum AttributeValue<'a> { + /// Text attribute Text(&'a str), - Float(f32), - Int(i32), + + /// A float + Float(f64), + + /// Signed integer + Int(i64), + + /// Boolean Bool(bool), - Listener(RefCell<&'a mut dyn FnMut(UiEvent)>), - Any(&'a dyn AnyValue), + + /// A listener, like "onclick" + Listener(RefCell>>), + + /// An arbitrary value that implements PartialEq and is static + Any(BumpBox<'a, dyn AnyValue>), + + /// A "none" value, resulting in the removal of an attribute from the dom None, } -impl<'a> AttributeValue<'a> { - pub fn new_listener( - cx: &'a ScopeState, - mut f: impl FnMut(UiEvent) + 'a, - ) -> AttributeValue<'a> { - AttributeValue::Listener(RefCell::new(cx.bump().alloc( - move |event: UiEvent| { - if let Ok(data) = event.data.downcast::() { - f(UiEvent { - bubbles: event.bubbles, - data, - }) - } - }, - ))) - } -} +type ListenerCb<'a> = BumpBox<'a, dyn FnMut(Event) + 'a>; impl<'a> std::fmt::Debug for AttributeValue<'a> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -192,26 +340,13 @@ impl<'a> PartialEq for AttributeValue<'a> { (Self::Int(l0), Self::Int(r0)) => l0 == r0, (Self::Bool(l0), Self::Bool(r0)) => l0 == r0, (Self::Listener(_), Self::Listener(_)) => true, - (Self::Any(l0), Self::Any(r0)) => l0.any_cmp(*r0), + (Self::Any(l0), Self::Any(r0)) => l0.any_cmp(r0.as_ref()), _ => core::mem::discriminant(self) == core::mem::discriminant(other), } } } -impl<'a> AttributeValue<'a> { - pub fn matches_type(&self, other: &'a AttributeValue<'a>) -> bool { - match (self, other) { - (Self::Text(_), Self::Text(_)) => true, - (Self::Float(_), Self::Float(_)) => true, - (Self::Int(_), Self::Int(_)) => true, - (Self::Bool(_), Self::Bool(_)) => true, - (Self::Listener(_), Self::Listener(_)) => true, - (Self::Any(_), Self::Any(_)) => true, - _ => return false, - } - } -} - +#[doc(hidden)] pub trait AnyValue { fn any_cmp(&self, other: &dyn AnyValue) -> bool; fn our_typeid(&self) -> TypeId; @@ -231,9 +366,185 @@ impl AnyValue for T { } } -#[test] -fn what_are_the_sizes() { - dbg!(std::mem::size_of::()); - dbg!(std::mem::size_of::