Subtree memorization / reactive templates (#488)

This commit adds subtree memoization to Dioxus.

Subtree memoization is basically a compile-time step that drastically 
reduces the amount of work the diffing engine needs to do at runtime by
extracting non-changing nodes out into a static "template." Templates 
are then understood by the various renderers in the ecosystem as a 
faster way of rendering the same items. 

For example, in the web, templates are simply a set of DOM Nodes created 
once and then cloned later. This is the same pattern frameworks like Lithtml
and SolidJS use to achieve near-perfect performance. 

Subtree memoization adds an additional level of complexity to Dioxus. The RSX
macro needs to be much smarter to identify changing/nonchanging nodes and
generate a mapping between the Template and its runtime counterparts.

This commit represents a working starter point for this work, adding support 
for templates for the web, desktop, liveview, ssr, and native-core renderers.
In the future we will try to shrink code generation, generally improve 
performance, and simplify our implementation.
This commit is contained in:
Demonthos 2022-09-30 14:03:06 -05:00 committed by GitHub
parent b32fd2d2cd
commit 047ed1e553
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
80 changed files with 6774 additions and 2552 deletions

View file

@ -17,7 +17,6 @@ members = [
"packages/liveview",
"packages/autofmt",
"packages/rsx",
"packages/rsx_interpreter",
"packages/native-core",
"packages/native-core-macro",
"docs/guide",
@ -42,7 +41,7 @@ rust-version = "1.60.0"
[dev-dependencies]
dioxus = { path = "./packages/dioxus" }
dioxus-desktop = { path = "./packages/desktop" }
dioxus-desktop = { path = "./packages/desktop", features = ["hot-reload"] }
dioxus-ssr = { path = "./packages/ssr" }
dioxus-router = { path = "./packages/router" }
fermi = { path = "./packages/fermi" }
@ -60,3 +59,8 @@ reqwest = { version = "0.11.9", features = ["json"] }
fern = { version = "0.6.0", features = ["colored"] }
thiserror = "1.0.30"
env_logger = "0.9.0"
[profile.release]
opt-level = 3
lto = true
debug = true

View file

@ -50,7 +50,7 @@ Generally, here's the status of each platform:
| 1st class global state | ✅ | redux/recoil/mobx on top of context |
| Runs natively | ✅ | runs as a portable binary w/o a runtime (Node) |
| Subtree Memoization | ✅ | skip diffing static element subtrees |
| High-efficiency templates | 🛠 | rsx! calls are translated to templates on the DOM's side |
| High-efficiency templates | | rsx! calls are translated to templates on the DOM's side |
| Compile-time correct | ✅ | Throw errors on invalid template layouts |
| Heuristic Engine | ✅ | track component memory usage to minimize future allocations |
| Fine-grained reactivity | 👀 | Skip diffing for fine-grain updates |
@ -66,7 +66,7 @@ These Features are planned for the future of Dioxus:
### Core
- [x] Release of Dioxus Core
- [x] Upgrade documentation to include more theory and be more comprehensive
- [ ] Support for HTML-side templates for lightning-fast dom manipulation
- [x] Support for HTML-side templates for lightning-fast dom manipulation
- [ ] Support for multiple renderers for same virtualdom (subtrees)
- [ ] Support for ThreadSafe (Send + Sync)
- [ ] Support for Portals

View file

@ -3,7 +3,7 @@ use std::{
fmt::{Result, Write},
};
use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed};
use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed, IfmtInput};
use proc_macro2::{LineColumn, Span};
use syn::{spanned::Spanned, Expr};
@ -72,8 +72,8 @@ impl Buffer {
}
}
pub fn write_text(&mut self, text: &syn::LitStr) -> Result {
write!(self.buf, "\"{}\"", text.value())
pub fn write_text(&mut self, text: &IfmtInput) -> Result {
write!(self.buf, "\"{}\"", text.source.as_ref().unwrap().value())
}
pub fn consume(self) -> Option<String> {
@ -155,13 +155,13 @@ impl Buffer {
total += match &attr.attr {
ElementAttr::AttrText { value, name } => {
value.value().len() + name.span().line_length() + 3
value.source.as_ref().unwrap().value().len() + name.span().line_length() + 3
}
ElementAttr::AttrExpression { name, value } => {
value.span().line_length() + name.span().line_length() + 3
}
ElementAttr::CustomAttrText { value, name } => {
value.value().len() + name.value().len() + 3
value.source.as_ref().unwrap().value().len() + name.value().len() + 3
}
ElementAttr::CustomAttrExpression { name, value } => {
name.value().len() + value.span().line_length() + 3

View file

@ -167,7 +167,12 @@ impl Buffer {
write!(self.buf, "{}: {}", name, out)?;
}
ContentField::Formatted(s) => {
write!(self.buf, "{}: \"{}\"", name, s.value())?;
write!(
self.buf,
"{}: \"{}\"",
name,
s.source.as_ref().unwrap().value()
)?;
}
ContentField::OnHandlerRaw(exp) => {
let out = prettyplease::unparse_expr(exp);
@ -209,7 +214,7 @@ impl Buffer {
let attr_len = fields
.iter()
.map(|field| match &field.content {
ContentField::Formatted(s) => s.value().len() ,
ContentField::Formatted(s) => s.source.as_ref().unwrap().value().len() ,
ContentField::OnHandlerRaw(exp) | ContentField::ManExpr(exp) => {
let formatted = prettyplease::unparse_expr(exp);
let len = if formatted.contains('\n') {

View file

@ -132,7 +132,7 @@ impl Buffer {
fn write_attributes(
&mut self,
attributes: &[ElementAttrNamed],
key: &Option<syn::LitStr>,
key: &Option<IfmtInput>,
sameline: bool,
) -> Result {
let mut attr_iter = attributes.iter().peekable();
@ -141,7 +141,11 @@ impl Buffer {
if !sameline {
self.indented_tabbed_line()?;
}
write!(self.buf, "key: \"{}\"", key.value())?;
write!(
self.buf,
"key: \"{}\"",
key.source.as_ref().unwrap().value()
)?;
if !attributes.is_empty() {
write!(self.buf, ",")?;
if sameline {
@ -178,7 +182,11 @@ impl Buffer {
fn write_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
match &attr.attr {
ElementAttr::AttrText { name, value } => {
write!(self.buf, "{name}: \"{value}\"", value = value.value())?;
write!(
self.buf,
"{name}: \"{value}\"",
value = value.source.as_ref().unwrap().value()
)?;
}
ElementAttr::AttrExpression { name, value } => {
let out = prettyplease::unparse_expr(value);
@ -190,7 +198,7 @@ impl Buffer {
self.buf,
"\"{name}\": \"{value}\"",
name = name.value(),
value = value.value()
value = value.source.as_ref().unwrap().value()
)?;
}
@ -272,7 +280,7 @@ impl Buffer {
}
match children {
[BodyNode::Text(ref text)] => Some(text.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);

View file

@ -19,7 +19,6 @@ proc-macro2 = { version = "1.0" }
quote = "1.0"
syn = { version = "1.0", features = ["full", "extra-traits"] }
dioxus-rsx = { path = "../rsx" }
dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true }
# testing
[dev-dependencies]
@ -28,4 +27,3 @@ trybuild = "1.0"
[features]
default = []
hot-reload = ["dioxus-rsx-interpreter"]

View file

@ -34,40 +34,21 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token
/// ```
#[proc_macro]
pub fn rsx(s: TokenStream) -> TokenStream {
#[cfg(feature = "hot-reload")]
let rsx_text = s.to_string();
match syn::parse::<rsx::CallBody>(s) {
Err(err) => err.to_compile_error().into(),
Ok(body) => body.to_token_stream().into(),
}
}
/// A version of the rsx! macro that does not use templates. Used for testing diffing
#[proc_macro]
pub fn rsx_without_templates(s: TokenStream) -> TokenStream {
match syn::parse::<rsx::CallBody>(s) {
Err(err) => err.to_compile_error().into(),
Ok(body) => {
#[cfg(feature = "hot-reload")]
{
use dioxus_rsx_interpreter::captuered_context::CapturedContextBuilder;
match CapturedContextBuilder::from_call_body(body) {
Ok(captured) => {
let lazy = quote::quote! {
LazyNodes::new(move |__cx|{
let code_location = get_line_num!();
let captured = #captured;
let text = #rsx_text;
resolve_scope(code_location, text, captured, __cx)
})
};
if let Some(cx) = captured.custom_context {
quote::quote! {
#cx.render(#lazy)
}
.into()
} else {
lazy.into()
}
}
Err(err) => err.into_compile_error().into(),
}
}
#[cfg(not(feature = "hot-reload"))]
body.to_token_stream().into()
let mut tokens = proc_macro2::TokenStream::new();
body.to_tokens_without_template(&mut tokens);
tokens.into()
}
}
}

View file

@ -48,4 +48,5 @@ backtrace = "0.3"
[features]
default = []
serialize = ["serde"]
debug_vdom = []
debug_vdom = []
hot-reload = []

View file

@ -233,3 +233,204 @@ impl<'a> AttributeValue<'a> {
}
}
}
/// A owned attribute value.
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(
all(feature = "serialize"),
derive(serde::Serialize, serde::Deserialize)
)]
#[allow(missing_docs)]
pub enum OwnedAttributeValue {
Text(String),
Float32(f32),
Float64(f64),
Int32(i32),
Int64(i64),
Uint32(u32),
Uint64(u64),
Bool(bool),
Vec3Float(f32, f32, f32),
Vec3Int(i32, i32, i32),
Vec3Uint(u32, u32, u32),
Vec4Float(f32, f32, f32, f32),
Vec4Int(i32, i32, i32, i32),
Vec4Uint(u32, u32, u32, u32),
Bytes(Vec<u8>),
// TODO: support other types
// Any(ArbitraryAttributeValue<'a>),
}
impl PartialEq<AttributeValue<'_>> for OwnedAttributeValue {
fn eq(&self, other: &AttributeValue<'_>) -> bool {
match (self, other) {
(Self::Text(l0), AttributeValue::Text(r0)) => l0 == r0,
(Self::Float32(l0), AttributeValue::Float32(r0)) => l0 == r0,
(Self::Float64(l0), AttributeValue::Float64(r0)) => l0 == r0,
(Self::Int32(l0), AttributeValue::Int32(r0)) => l0 == r0,
(Self::Int64(l0), AttributeValue::Int64(r0)) => l0 == r0,
(Self::Uint32(l0), AttributeValue::Uint32(r0)) => l0 == r0,
(Self::Uint64(l0), AttributeValue::Uint64(r0)) => l0 == r0,
(Self::Bool(l0), AttributeValue::Bool(r0)) => l0 == r0,
(Self::Vec3Float(l0, l1, l2), AttributeValue::Vec3Float(r0, r1, r2)) => {
l0 == r0 && l1 == r1 && l2 == r2
}
(Self::Vec3Int(l0, l1, l2), AttributeValue::Vec3Int(r0, r1, r2)) => {
l0 == r0 && l1 == r1 && l2 == r2
}
(Self::Vec3Uint(l0, l1, l2), AttributeValue::Vec3Uint(r0, r1, r2)) => {
l0 == r0 && l1 == r1 && l2 == r2
}
(Self::Vec4Float(l0, l1, l2, l3), AttributeValue::Vec4Float(r0, r1, r2, r3)) => {
l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3
}
(Self::Vec4Int(l0, l1, l2, l3), AttributeValue::Vec4Int(r0, r1, r2, r3)) => {
l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3
}
(Self::Vec4Uint(l0, l1, l2, l3), AttributeValue::Vec4Uint(r0, r1, r2, r3)) => {
l0 == r0 && l1 == r1 && l2 == r2 && l3 == r3
}
(Self::Bytes(l0), AttributeValue::Bytes(r0)) => l0 == r0,
(_, _) => false,
}
}
}
impl<'a> From<AttributeValue<'a>> for OwnedAttributeValue {
fn from(attr: AttributeValue<'a>) -> Self {
match attr {
AttributeValue::Text(t) => OwnedAttributeValue::Text(t.to_owned()),
AttributeValue::Float32(f) => OwnedAttributeValue::Float32(f),
AttributeValue::Float64(f) => OwnedAttributeValue::Float64(f),
AttributeValue::Int32(i) => OwnedAttributeValue::Int32(i),
AttributeValue::Int64(i) => OwnedAttributeValue::Int64(i),
AttributeValue::Uint32(u) => OwnedAttributeValue::Uint32(u),
AttributeValue::Uint64(u) => OwnedAttributeValue::Uint64(u),
AttributeValue::Bool(b) => OwnedAttributeValue::Bool(b),
AttributeValue::Vec3Float(f1, f2, f3) => OwnedAttributeValue::Vec3Float(f1, f2, f3),
AttributeValue::Vec3Int(f1, f2, f3) => OwnedAttributeValue::Vec3Int(f1, f2, f3),
AttributeValue::Vec3Uint(f1, f2, f3) => OwnedAttributeValue::Vec3Uint(f1, f2, f3),
AttributeValue::Vec4Float(f1, f2, f3, f4) => {
OwnedAttributeValue::Vec4Float(f1, f2, f3, f4)
}
AttributeValue::Vec4Int(f1, f2, f3, f4) => OwnedAttributeValue::Vec4Int(f1, f2, f3, f4),
AttributeValue::Vec4Uint(f1, f2, f3, f4) => {
OwnedAttributeValue::Vec4Uint(f1, f2, f3, f4)
}
AttributeValue::Bytes(b) => OwnedAttributeValue::Bytes(b.to_owned()),
AttributeValue::Any(_) => todo!(),
}
}
}
// todo
#[allow(missing_docs)]
impl OwnedAttributeValue {
pub fn as_text(&self) -> Option<&str> {
match self {
OwnedAttributeValue::Text(s) => Some(s),
_ => None,
}
}
pub fn as_float32(&self) -> Option<f32> {
match self {
OwnedAttributeValue::Float32(f) => Some(*f),
_ => None,
}
}
pub fn as_float64(&self) -> Option<f64> {
match self {
OwnedAttributeValue::Float64(f) => Some(*f),
_ => None,
}
}
pub fn as_int32(&self) -> Option<i32> {
match self {
OwnedAttributeValue::Int32(i) => Some(*i),
_ => None,
}
}
pub fn as_int64(&self) -> Option<i64> {
match self {
OwnedAttributeValue::Int64(i) => Some(*i),
_ => None,
}
}
pub fn as_uint32(&self) -> Option<u32> {
match self {
OwnedAttributeValue::Uint32(i) => Some(*i),
_ => None,
}
}
pub fn as_uint64(&self) -> Option<u64> {
match self {
OwnedAttributeValue::Uint64(i) => Some(*i),
_ => None,
}
}
pub fn as_bool(&self) -> Option<bool> {
match self {
OwnedAttributeValue::Bool(b) => Some(*b),
_ => None,
}
}
pub fn as_vec3_float(&self) -> Option<(f32, f32, f32)> {
match self {
OwnedAttributeValue::Vec3Float(x, y, z) => Some((*x, *y, *z)),
_ => None,
}
}
pub fn as_vec3_int(&self) -> Option<(i32, i32, i32)> {
match self {
OwnedAttributeValue::Vec3Int(x, y, z) => Some((*x, *y, *z)),
_ => None,
}
}
pub fn as_vec3_uint(&self) -> Option<(u32, u32, u32)> {
match self {
OwnedAttributeValue::Vec3Uint(x, y, z) => Some((*x, *y, *z)),
_ => None,
}
}
pub fn as_vec4_float(&self) -> Option<(f32, f32, f32, f32)> {
match self {
OwnedAttributeValue::Vec4Float(x, y, z, w) => Some((*x, *y, *z, *w)),
_ => None,
}
}
pub fn as_vec4_int(&self) -> Option<(i32, i32, i32, i32)> {
match self {
OwnedAttributeValue::Vec4Int(x, y, z, w) => Some((*x, *y, *z, *w)),
_ => None,
}
}
pub fn as_vec4_uint(&self) -> Option<(u32, u32, u32, u32)> {
match self {
OwnedAttributeValue::Vec4Uint(x, y, z, w) => Some((*x, *y, *z, *w)),
_ => None,
}
}
pub fn as_bytes(&self) -> Option<&[u8]> {
match self {
OwnedAttributeValue::Bytes(b) => Some(b),
_ => None,
}
}
}

View file

@ -91,10 +91,19 @@
//! More info on how to improve this diffing algorithm:
//! - <https://hacks.mozilla.org/2019/03/fast-bump-allocated-virtual-doms-with-rust-and-wasm/>
use crate::innerlude::{
AnyProps, ElementId, Mutations, ScopeArena, ScopeId, VComponent, VElement, VFragment, VNode,
VPlaceholder, VText,
use crate::{
dynamic_template_context::TemplateContext,
innerlude::{
AnyProps, ElementId, GlobalNodeId, Mutations, RendererTemplateId, ScopeArena, ScopeId,
VComponent, VElement, VFragment, VNode, VPlaceholder, VText,
},
template::{
Template, TemplateAttribute, TemplateElement, TemplateNode, TemplateNodeId,
TemplateNodeType, TemplateValue, TextTemplateSegment, VTemplateRef,
},
Attribute, TemplateAttributeValue,
};
use bumpalo::Bump;
use fxhash::{FxHashMap, FxHashSet};
use smallvec::{smallvec, SmallVec};
@ -102,7 +111,7 @@ pub(crate) struct DiffState<'bump> {
pub(crate) scopes: &'bump ScopeArena,
pub(crate) mutations: Mutations<'bump>,
pub(crate) force_diff: bool,
pub(crate) element_stack: SmallVec<[ElementId; 10]>,
pub(crate) element_stack: SmallVec<[GlobalNodeId; 10]>,
pub(crate) scope_stack: SmallVec<[ScopeId; 5]>,
}
@ -133,7 +142,7 @@ impl<'b> DiffState<'b> {
}
pub fn diff_node(&mut self, old_node: &'b VNode<'b>, new_node: &'b VNode<'b>) {
use VNode::{Component, Element, Fragment, Placeholder, Text};
use VNode::{Component, Element, Fragment, Placeholder, TemplateRef, Text};
match (old_node, new_node) {
(Text(old), Text(new)) => {
self.diff_text_nodes(old, new, old_node, new_node);
@ -148,16 +157,20 @@ impl<'b> DiffState<'b> {
}
(Component(old), Component(new)) => {
self.diff_component_nodes(old_node, new_node, *old, *new);
self.diff_component_nodes(*old, *new, old_node, new_node);
}
(Fragment(old), Fragment(new)) => {
self.diff_fragment_nodes(old, new);
}
(TemplateRef(old), TemplateRef(new)) => {
self.diff_template_ref_nodes(old, new, old_node, new_node);
}
(
Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_),
Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_),
Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_) | TemplateRef(_),
Component(_) | Fragment(_) | Text(_) | Element(_) | Placeholder(_) | TemplateRef(_),
) => self.replace_node(old_node, new_node),
}
}
@ -169,6 +182,7 @@ impl<'b> DiffState<'b> {
VNode::Element(element) => self.create_element_node(element, node),
VNode::Fragment(frag) => self.create_fragment_node(frag),
VNode::Component(component) => self.create_component_node(*component),
VNode::TemplateRef(temp) => self.create_template_ref_node(*temp, node),
}
}
@ -204,19 +218,21 @@ impl<'b> DiffState<'b> {
dom_id.set(Some(real_id));
self.element_stack.push(real_id);
self.element_stack.push(GlobalNodeId::VNodeId(real_id));
{
self.mutations.create_element(tag_name, *namespace, real_id);
let cur_scope_id = self.current_scope();
for listener in listeners.iter() {
listener.mounted_node.set(Some(real_id));
listener
.mounted_node
.set(Some(GlobalNodeId::VNodeId(real_id)));
self.mutations.new_event_listener(listener, cur_scope_id);
}
for attr in attributes.iter() {
self.mutations.set_attribute(attr, real_id.as_u64());
self.mutations.set_attribute(attr, real_id);
}
if !children.is_empty() {
@ -249,7 +265,6 @@ impl<'b> DiffState<'b> {
props,
Some(parent_idx),
self.element_stack.last().copied().unwrap(),
0,
)
};
@ -286,6 +301,37 @@ impl<'b> DiffState<'b> {
created
}
pub(crate) fn create_template_ref_node(
&mut self,
new: &'b VTemplateRef<'b>,
node: &'b VNode<'b>,
) -> usize {
let (id, created) = {
let mut resolver = self.scopes.template_resolver.borrow_mut();
resolver.get_or_create_client_id(&new.template_id)
};
let template = {
let templates = self.scopes.templates.borrow();
templates.get(&new.template_id).unwrap().clone()
};
let template = template.borrow();
if created {
self.register_template(&template, id);
}
let real_id = self.scopes.reserve_node(node);
new.id.set(Some(real_id));
self.mutations.create_template_ref(real_id, id.into());
new.hydrate(&template, self);
1
}
pub(crate) fn diff_text_nodes(
&mut self,
old: &'b VText<'b>,
@ -304,7 +350,7 @@ impl<'b> DiffState<'b> {
};
if old.text != new.text {
self.mutations.set_text(new.text, root.as_u64());
self.mutations.set_text(new.text, root);
}
self.scopes.update_node(new_node, root);
@ -377,16 +423,18 @@ impl<'b> DiffState<'b> {
// TODO: take a more efficient path than this
if old.attributes.len() == new.attributes.len() {
for (old_attr, new_attr) in old.attributes.iter().zip(new.attributes.iter()) {
if old_attr.value != new_attr.value || new_attr.is_volatile {
self.mutations.set_attribute(new_attr, root.as_u64());
if !old_attr.is_static && old_attr.value != new_attr.value
|| new_attr.attribute.volatile
{
self.mutations.set_attribute(new_attr, root);
}
}
} else {
for attribute in old.attributes {
self.mutations.remove_attribute(attribute, root.as_u64());
self.mutations.remove_attribute(attribute, root);
}
for attribute in new.attributes {
self.mutations.set_attribute(attribute, root.as_u64());
self.mutations.set_attribute(attribute, root);
}
}
@ -404,18 +452,16 @@ impl<'b> DiffState<'b> {
for (old_l, new_l) in old.listeners.iter().zip(new.listeners.iter()) {
new_l.mounted_node.set(old_l.mounted_node.get());
if old_l.event != new_l.event {
self.mutations
.remove_event_listener(old_l.event, root.as_u64());
self.mutations.remove_event_listener(old_l.event, root);
self.mutations.new_event_listener(new_l, cur_scope_id);
}
}
} else {
for listener in old.listeners {
self.mutations
.remove_event_listener(listener.event, root.as_u64());
self.mutations.remove_event_listener(listener.event, root);
}
for listener in new.listeners {
listener.mounted_node.set(Some(root));
listener.mounted_node.set(Some(GlobalNodeId::VNodeId(root)));
self.mutations.new_event_listener(listener, cur_scope_id);
}
}
@ -434,10 +480,10 @@ impl<'b> DiffState<'b> {
fn diff_component_nodes(
&mut self,
old_node: &'b VNode<'b>,
new_node: &'b VNode<'b>,
old: &'b VComponent<'b>,
new: &'b VComponent<'b>,
old_node: &'b VNode<'b>,
new_node: &'b VNode<'b>,
) {
let scope_addr = old
.scope
@ -526,6 +572,228 @@ impl<'b> DiffState<'b> {
self.diff_children(old.children, new.children);
}
fn diff_template_ref_nodes(
&mut self,
old: &'b VTemplateRef<'b>,
new: &'b VTemplateRef<'b>,
old_node: &'b VNode<'b>,
new_node: &'b VNode<'b>,
) {
if std::ptr::eq(old, new) {
return;
}
// if the templates are different, just rebuild it
if old.template_id != new.template_id
|| self
.scopes
.template_resolver
.borrow()
.is_dirty(&new.template_id)
{
self.replace_node(old_node, new_node);
return;
}
// if the node is comming back not assigned, that means it was borrowed but removed
let root = match old.id.get() {
Some(id) => id,
None => self.scopes.reserve_node(new_node),
};
self.scopes.update_node(new_node, root);
new.id.set(Some(root));
self.element_stack.push(GlobalNodeId::VNodeId(root));
self.mutations.enter_template_ref(root);
let scope_bump = &self.current_scope_bump();
let template = {
let templates = self.scopes.templates.borrow();
templates.get(&new.template_id).unwrap().clone()
};
let template = template.borrow();
fn diff_attributes<'b, Nodes, Attributes, V, Children, Listeners, TextSegments, Text>(
nodes: &Nodes,
ctx: (
&mut Mutations<'b>,
&'b Bump,
&VTemplateRef<'b>,
&Template,
usize,
),
) where
Nodes: AsRef<[TemplateNode<Attributes, V, Children, Listeners, TextSegments, Text>]>,
Attributes: AsRef<[TemplateAttribute<V>]>,
V: TemplateValue,
Children: AsRef<[TemplateNodeId]>,
Listeners: AsRef<[usize]>,
TextSegments: AsRef<[TextTemplateSegment<Text>]>,
Text: AsRef<str>,
{
let (mutations, scope_bump, new, template, idx) = ctx;
for (node_id, attr_idx) in template.get_dynamic_nodes_for_attribute_index(idx) {
if let TemplateNodeType::Element(el) = &nodes.as_ref()[node_id.0].node_type {
let TemplateElement { attributes, .. } = el;
let attr = &attributes.as_ref()[*attr_idx];
let attribute = Attribute {
attribute: attr.attribute,
value: new.dynamic_context.resolve_attribute(idx).clone(),
is_static: false,
};
mutations.set_attribute(scope_bump.alloc(attribute), *node_id);
} else {
panic!("expected element node");
}
}
}
// diff dynamic attributes
for (idx, (old_attr, new_attr)) in old
.dynamic_context
.attributes
.iter()
.zip(new.dynamic_context.attributes.iter())
.enumerate()
{
if old_attr != new_attr {
template.with_nodes(
diff_attributes,
diff_attributes,
(&mut self.mutations, scope_bump, new, &template, idx),
);
}
}
fn set_attribute<'b, Attributes, V, Children, Listeners, TextSegments, Text>(
node: &TemplateNode<Attributes, V, Children, Listeners, TextSegments, Text>,
ctx: (&mut Mutations<'b>, &'b Bump, &VTemplateRef<'b>, usize),
) where
Attributes: AsRef<[TemplateAttribute<V>]>,
V: TemplateValue,
Children: AsRef<[TemplateNodeId]>,
Listeners: AsRef<[usize]>,
TextSegments: AsRef<[TextTemplateSegment<Text>]>,
Text: AsRef<str>,
{
let (mutations, scope_bump, new, template_attr_idx) = ctx;
if let TemplateNodeType::Element(el) = &node.node_type {
let TemplateElement { attributes, .. } = el;
let attr = &attributes.as_ref()[template_attr_idx];
let value = match &attr.value {
TemplateAttributeValue::Dynamic(idx) => {
new.dynamic_context.resolve_attribute(*idx).clone()
}
TemplateAttributeValue::Static(value) => value.allocate(scope_bump),
};
let attribute = Attribute {
attribute: attr.attribute,
value,
is_static: false,
};
mutations.set_attribute(scope_bump.alloc(attribute), node.id);
} else {
panic!("expected element node");
}
}
// set all volatile attributes
for (id, idx) in template.volatile_attributes() {
template.with_node(
id,
set_attribute,
set_attribute,
(&mut self.mutations, scope_bump, new, idx),
)
}
fn diff_dynamic_node<'b, Attributes, V, Children, Listeners, TextSegments, Text>(
node: &TemplateNode<Attributes, V, Children, Listeners, TextSegments, Text>,
ctx: (&mut DiffState<'b>, &'b VNode<'b>, &'b VNode<'b>, ElementId),
) where
Attributes: AsRef<[TemplateAttribute<V>]>,
V: TemplateValue,
Children: AsRef<[TemplateNodeId]>,
Listeners: AsRef<[usize]>,
TextSegments: AsRef<[TextTemplateSegment<Text>]>,
Text: AsRef<str>,
{
let (diff, old_node, new_node, root) = ctx;
if let TemplateNodeType::Element { .. } = node.node_type {
diff.element_stack.push(GlobalNodeId::VNodeId(root));
diff.diff_node(old_node, new_node);
diff.element_stack.pop();
} else {
diff.diff_node(old_node, new_node);
}
}
// diff dynmaic nodes
for (idx, (old_node, new_node)) in old
.dynamic_context
.nodes
.iter()
.zip(new.dynamic_context.nodes.iter())
.enumerate()
{
if let Some(id) = template.get_dynamic_nodes_for_node_index(idx) {
template.with_node(
id,
diff_dynamic_node,
diff_dynamic_node,
(self, old_node, new_node, root),
);
}
}
// diff dynamic text
// text nodes could rely on multiple dynamic text parts, so we keep a record of which ones to rerender and send the diffs at the end
let mut dirty_text_nodes = FxHashSet::default();
for (idx, (old_text, new_text)) in old
.dynamic_context
.text_segments
.iter()
.zip(new.dynamic_context.text_segments.iter())
.enumerate()
{
if old_text != new_text {
for node_id in template.get_dynamic_nodes_for_text_index(idx) {
dirty_text_nodes.insert(*node_id);
}
}
}
for node_id in dirty_text_nodes {
fn diff_text<'b, Attributes, V, Children, Listeners, TextSegments, Text>(
node: &TemplateNode<Attributes, V, Children, Listeners, TextSegments, Text>,
ctx: (&mut DiffState<'b>, &TemplateContext<'b>),
) where
Attributes: AsRef<[TemplateAttribute<V>]>,
V: TemplateValue,
Children: AsRef<[TemplateNodeId]>,
Listeners: AsRef<[usize]>,
TextSegments: AsRef<[TextTemplateSegment<Text>]>,
Text: AsRef<str>,
{
let (diff, dynamic_context) = ctx;
if let TemplateNodeType::Text(text) = &node.node_type {
let text = dynamic_context.resolve_text(&text.segments.as_ref());
diff.mutations
.set_text(diff.current_scope_bump().alloc(text), node.id);
} else {
panic!("expected text node");
}
}
template.with_node(node_id, diff_text, diff_text, (self, &new.dynamic_context));
}
self.mutations.exit_template_ref();
self.element_stack.pop();
}
// Diff the given set of old and new children.
//
// The parent must be on top of the change list stack when this function is
@ -970,6 +1238,16 @@ impl<'b> DiffState<'b> {
}
self.leave_scope();
}
VNode::TemplateRef(t) => {
let id = old
.try_mounted_id()
.unwrap_or_else(|| panic!("broke on {:?}", old));
self.mutations.replace_with(id, nodes_created as u32);
self.remove_nodes(t.dynamic_context.nodes, true);
self.scopes.collect_garbage(id);
}
}
}
@ -983,7 +1261,7 @@ impl<'b> DiffState<'b> {
t.id.set(None);
if gen_muts {
self.mutations.remove(id.as_u64());
self.mutations.remove(id);
}
}
}
@ -993,14 +1271,14 @@ impl<'b> DiffState<'b> {
a.id.set(None);
if gen_muts {
self.mutations.remove(id.as_u64());
self.mutations.remove(id);
}
}
VNode::Element(e) => {
let id = e.id.get().unwrap();
if gen_muts {
self.mutations.remove(id.as_u64());
self.mutations.remove(id);
}
self.scopes.collect_garbage(id);
@ -1029,6 +1307,19 @@ impl<'b> DiffState<'b> {
}
self.leave_scope();
}
VNode::TemplateRef(t) => {
let id = t.id.get().unwrap();
if gen_muts {
self.mutations.remove(id);
}
self.scopes.collect_garbage(id);
t.id.set(None);
self.remove_nodes(t.dynamic_context.nodes, gen_muts);
}
}
}
}
@ -1058,7 +1349,7 @@ impl<'b> DiffState<'b> {
self.mutations.insert_before(first, created as u32);
}
fn current_scope(&self) -> ScopeId {
pub fn current_scope(&self) -> ScopeId {
self.scope_stack.last().copied().expect("no current scope")
}
@ -1082,6 +1373,7 @@ impl<'b> DiffState<'b> {
let scope_id = el.scope.get().unwrap();
search_node = Some(self.scopes.root_node(scope_id));
}
VNode::TemplateRef(t) => break t.id.get(),
}
}
}
@ -1098,6 +1390,7 @@ impl<'b> DiffState<'b> {
let scope = el.scope.get().expect("element to have a scope assigned");
search_node = Some(self.scopes.root_node(scope));
}
VNode::TemplateRef(t) => break t.id.get(),
}
}
}
@ -1105,7 +1398,7 @@ impl<'b> DiffState<'b> {
// recursively push all the nodes of a tree onto the stack and return how many are there
fn push_all_real_nodes(&mut self, node: &'b VNode<'b>) -> usize {
match node {
VNode::Text(_) | VNode::Placeholder(_) | VNode::Element(_) => {
VNode::Text(_) | VNode::Placeholder(_) | VNode::Element(_) | VNode::TemplateRef(_) => {
self.mutations.push_root(node.mounted_id());
1
}
@ -1125,4 +1418,18 @@ impl<'b> DiffState<'b> {
}
}
}
pub(crate) fn current_scope_bump(&self) -> &'b Bump {
&self
.scopes
.get_scope(self.current_scope())
.unwrap()
.fin_frame()
.bump
}
pub fn register_template(&mut self, template: &Template, id: RendererTemplateId) {
let bump = &self.scopes.template_bump;
template.create(&mut self.mutations, bump, id);
}
}

View file

@ -0,0 +1,195 @@
use std::{marker::PhantomData, ops::Deref};
use once_cell::sync::Lazy;
use crate::{
template::{TemplateNodeId, TextTemplateSegment},
AttributeValue, Listener, VNode,
};
/// A lazily initailized vector
#[derive(Debug, Clone, Copy)]
pub struct LazyStaticVec<T: 'static>(pub &'static Lazy<Vec<T>>);
impl<T: 'static> AsRef<[T]> for LazyStaticVec<T> {
fn as_ref(&self) -> &[T] {
let v: &Vec<_> = self.0.deref();
v.as_ref()
}
}
impl<T> PartialEq for LazyStaticVec<T> {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(self.0, other.0)
}
}
/// Stores what nodes depend on specific dynamic parts of the template to allow the diffing algorithm to jump to that part of the template instead of travering it
/// This makes adding constant template nodes add no additional cost to diffing.
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
pub struct DynamicNodeMapping<
Nodes,
TextOuter,
TextInner,
AttributesOuter,
AttributesInner,
Volatile,
Listeners,
> where
Nodes: AsRef<[Option<TemplateNodeId>]>,
TextOuter: AsRef<[TextInner]>,
TextInner: AsRef<[TemplateNodeId]>,
AttributesOuter: AsRef<[AttributesInner]>,
AttributesInner: AsRef<[(TemplateNodeId, usize)]>,
Volatile: AsRef<[(TemplateNodeId, usize)]>,
Listeners: AsRef<[TemplateNodeId]>,
{
/// The node that depend on each node in the dynamic template
pub nodes: Nodes,
text_inner: PhantomData<TextInner>,
/// The text nodes that depend on each text segment of the dynamic template
pub text: TextOuter,
/// The attributes along with the attribute index in the template that depend on each attribute of the dynamic template
pub attributes: AttributesOuter,
attributes_inner: PhantomData<AttributesInner>,
/// The attributes that are marked as volatile in the template
pub volatile_attributes: Volatile,
/// The listeners that depend on each listener of the dynamic template
pub nodes_with_listeners: Listeners,
}
impl<Nodes, TextOuter, TextInner, AttributesOuter, AttributesInner, Volatile, Listeners>
DynamicNodeMapping<
Nodes,
TextOuter,
TextInner,
AttributesOuter,
AttributesInner,
Volatile,
Listeners,
>
where
Nodes: AsRef<[Option<TemplateNodeId>]>,
TextOuter: AsRef<[TextInner]>,
TextInner: AsRef<[TemplateNodeId]>,
AttributesOuter: AsRef<[AttributesInner]>,
AttributesInner: AsRef<[(TemplateNodeId, usize)]>,
Volatile: AsRef<[(TemplateNodeId, usize)]>,
Listeners: AsRef<[TemplateNodeId]>,
{
/// Creates a new dynamic node mapping
pub const fn new(
nodes: Nodes,
text: TextOuter,
attributes: AttributesOuter,
volatile_attributes: Volatile,
listeners: Listeners,
) -> Self {
DynamicNodeMapping {
nodes,
text_inner: PhantomData,
text,
attributes,
attributes_inner: PhantomData,
volatile_attributes,
nodes_with_listeners: listeners,
}
}
pub(crate) fn all_dynamic<'a>(&'a self) -> impl Iterator<Item = TemplateNodeId> + 'a {
self.nodes
.as_ref()
.iter()
.filter_map(|o| o.as_ref())
.chain(
self.text
.as_ref()
.iter()
.map(|ids| ids.as_ref().iter())
.flatten(),
)
.copied()
.chain(
self.attributes
.as_ref()
.iter()
.map(|ids| ids.as_ref().iter())
.flatten()
.map(|dynamic| dynamic.0),
)
.chain(self.nodes_with_listeners.as_ref().iter().copied())
}
}
/// A dynamic node mapping that is stack allocated
pub type StaticDynamicNodeMapping = DynamicNodeMapping<
&'static [Option<TemplateNodeId>],
&'static [&'static [TemplateNodeId]],
&'static [TemplateNodeId],
&'static [&'static [(TemplateNodeId, usize)]],
&'static [(TemplateNodeId, usize)],
// volatile attribute information is available at compile time, but there is no way for the macro to generate it, so we initialize it lazily instead
LazyStaticVec<(TemplateNodeId, usize)>,
&'static [TemplateNodeId],
>;
#[cfg(any(feature = "hot-reload", debug_assertions))]
/// A dynamic node mapping that is heap allocated
pub type OwnedDynamicNodeMapping = DynamicNodeMapping<
Vec<Option<TemplateNodeId>>,
Vec<Vec<TemplateNodeId>>,
Vec<TemplateNodeId>,
Vec<Vec<(TemplateNodeId, usize)>>,
Vec<(TemplateNodeId, usize)>,
Vec<(TemplateNodeId, usize)>,
Vec<TemplateNodeId>,
>;
/// The dynamic parts used to saturate a template durring runtime
pub struct TemplateContext<'b> {
/// The dynamic nodes
pub nodes: &'b [VNode<'b>],
/// The dynamic text
pub text_segments: &'b [&'b str],
/// The dynamic attributes
pub attributes: &'b [AttributeValue<'b>],
/// The dynamic attributes
// The listeners must not change during the lifetime of the context, use a dynamic node if the listeners change
pub listeners: &'b [Listener<'b>],
/// A optional key for diffing
pub key: Option<&'b str>,
}
impl<'b> TemplateContext<'b> {
/// Resolve text segments to a string
pub fn resolve_text<TextSegments, Text>(&self, text: &TextSegments) -> String
where
TextSegments: AsRef<[TextTemplateSegment<Text>]>,
Text: AsRef<str>,
{
let mut result = String::new();
for seg in text.as_ref() {
match seg {
TextTemplateSegment::Static(s) => result += s.as_ref(),
TextTemplateSegment::Dynamic(idx) => result += self.text_segments[*idx],
}
}
result
}
/// Resolve an attribute value
pub fn resolve_attribute(&self, idx: usize) -> &'b AttributeValue<'b> {
&self.attributes[idx]
}
/// Resolve a listener
pub fn resolve_listener(&self, idx: usize) -> &'b Listener<'b> {
&self.listeners[idx]
}
/// Resolve a node
pub fn resolve_node(&self, idx: usize) -> &'b VNode<'b> {
&self.nodes[idx]
}
}

View file

@ -3,7 +3,7 @@
//!
//! This is all kinda WIP, but the bones are there.
use crate::{ElementId, ScopeId};
use crate::{nodes::GlobalNodeId, ScopeId};
use std::{any::Any, cell::Cell, fmt::Debug, rc::Rc, sync::Arc};
pub(crate) struct BubbleState {
@ -58,7 +58,7 @@ pub struct UserEvent {
pub priority: EventPriority,
/// The optional real node associated with the trigger
pub element: Option<ElementId>,
pub element: Option<GlobalNodeId>,
/// The event type IE "onclick" or "onmouseover"
pub name: &'static str,

View file

@ -4,23 +4,27 @@
pub(crate) mod arbitrary_value;
pub(crate) mod diff;
pub(crate) mod dynamic_template_context;
pub(crate) mod events;
pub(crate) mod lazynodes;
pub(crate) mod mutations;
pub(crate) mod nodes;
pub(crate) mod properties;
pub(crate) mod scopes;
pub(crate) mod template;
pub(crate) mod util;
pub(crate) mod virtual_dom;
pub(crate) mod innerlude {
pub use crate::arbitrary_value::*;
pub use crate::dynamic_template_context::*;
pub use crate::events::*;
pub use crate::lazynodes::*;
pub use crate::mutations::*;
pub use crate::nodes::*;
pub use crate::properties::*;
pub use crate::scopes::*;
pub use crate::template::*;
pub use crate::util::*;
pub use crate::virtual_dom::*;
@ -63,20 +67,36 @@ pub(crate) mod innerlude {
}
pub use crate::innerlude::{
AnyEvent, ArbitraryAttributeValue, Attribute, AttributeValue, Component, DioxusElement,
DomEdit, Element, ElementId, ElementIdIterator, EventHandler, EventPriority, IntoVNode,
LazyNodes, Listener, Mutations, NodeFactory, Properties, SchedulerMsg, Scope, ScopeId,
ScopeState, TaskId, UiEvent, UserEvent, VComponent, VElement, VFragment, VNode, VPlaceholder,
VText, VirtualDom,
AnyEvent, ArbitraryAttributeValue, Attribute, AttributeDiscription, AttributeValue,
CodeLocation, Component, DioxusElement, DomEdit, DynamicNodeMapping, Element, ElementId,
ElementIdIterator, EventHandler, EventPriority, GlobalNodeId, IntoVNode, LazyNodes, Listener,
Mutations, NodeFactory, OwnedAttributeValue, Properties, RendererTemplateId, SchedulerMsg,
Scope, ScopeId, ScopeState, StaticCodeLocation, StaticDynamicNodeMapping, StaticTemplateNode,
StaticTemplateNodes, TaskId, Template, TemplateAttribute, TemplateAttributeValue,
TemplateContext, TemplateElement, TemplateId, TemplateNode, TemplateNodeId, TemplateNodeType,
TemplateValue, TextTemplate, TextTemplateSegment, UiEvent, UserEvent, VComponent, VElement,
VFragment, VNode, VPlaceholder, VText, VirtualDom, JS_MAX_INT,
};
#[cfg(any(feature = "hot-reload", debug_assertions))]
pub use crate::innerlude::{
OwnedCodeLocation, OwnedDynamicNodeMapping, OwnedTemplateNode, OwnedTemplateNodes,
SetTemplateMsg,
};
/// The purpose of this module is to alleviate imports of many common types
///
/// This includes types like [`Scope`], [`Element`], and [`Component`].
pub mod prelude {
pub use crate::get_line_num;
#[cfg(any(feature = "hot-reload", debug_assertions))]
pub use crate::innerlude::OwnedTemplate;
pub use crate::innerlude::{
fc_to_builder, Attributes, Component, DioxusElement, Element, EventHandler, Fragment,
LazyNodes, NodeFactory, Properties, Scope, ScopeId, ScopeState, VNode, VirtualDom,
fc_to_builder, AttributeDiscription, AttributeValue, Attributes, CodeLocation, Component,
DioxusElement, Element, EventHandler, Fragment, LazyNodes, LazyStaticVec, NodeFactory,
Properties, Scope, ScopeId, ScopeState, StaticAttributeValue, StaticCodeLocation,
StaticDynamicNodeMapping, StaticTemplate, StaticTemplateNodes, Template, TemplateAttribute,
TemplateAttributeValue, TemplateContext, TemplateElement, TemplateId, TemplateNode,
TemplateNodeId, TemplateNodeType, TextTemplate, TextTemplateSegment, VNode, VirtualDom,
};
}
@ -85,6 +105,7 @@ pub mod exports {
//! Feel free to just add the dependencies in your own Crates.toml
pub use bumpalo;
pub use futures_channel;
pub use once_cell;
}
/// Functions that wrap unsafe functionality to prevent us from misusing it at the callsite

View file

@ -128,6 +128,59 @@ pub enum DomEdit<'bump> {
root: u64,
},
/// Create a new purely-text node in a template
CreateTextNodeTemplate {
/// The ID the new node should have.
root: u64,
/// The textcontent of the noden
text: &'bump str,
/// If the id of the node must be kept in the refrences
locally_static: bool,
},
/// Create a new purely-element node in a template
CreateElementTemplate {
/// The ID the new node should have.
root: u64,
/// The tagname of the node
tag: &'bump str,
/// If the id of the node must be kept in the refrences
locally_static: bool,
/// If any children of this node must be kept in the references
fully_static: bool,
},
/// Create a new purely-comment node with a given namespace in a template
CreateElementNsTemplate {
/// The ID the new node should have.
root: u64,
/// The namespace of the node
tag: &'bump str,
/// The namespace of the node (like `SVG`)
ns: &'static str,
/// If the id of the node must be kept in the refrences
locally_static: bool,
/// If any children of this node must be kept in the references
fully_static: bool,
},
/// Create a new placeholder node.
/// In most implementations, this will either be a hidden div or a comment node. in a template
/// Always both locally and fully static
CreatePlaceholderTemplate {
/// The ID the new node should have.
root: u64,
},
/// Create a new Event Listener.
NewEventListener {
/// The name of the event to listen for.
@ -189,6 +242,38 @@ pub enum DomEdit<'bump> {
/// Manually pop a root node from the stack.
PopRoot {},
/// Enter a TemplateRef tree
EnterTemplateRef {
/// The ID of the node to enter.
root: u64,
},
/// Exit a TemplateRef tree
ExitTemplateRef {},
/// Create a refrence to a template node.
CreateTemplateRef {
/// The ID of the new template refrence.
id: u64,
/// The ID of the template the node is refrencing.
template_id: u64,
},
/// Create a new templete.
/// IMPORTANT: When adding nodes to a templete, id's will reset to zero, so they must be allocated on a different stack.
/// It is recommended to use Cow<NativeNode>.
CreateTemplate {
/// The ID of the new template.
id: u64,
},
/// Finish a templete
FinishTemplate {
/// The number of root nodes in the template
len: u32,
},
}
use fxhash::FxHashSet;
@ -204,8 +289,8 @@ impl<'a> Mutations<'a> {
}
// Navigation
pub(crate) fn push_root(&mut self, root: ElementId) {
let id = root.as_u64();
pub(crate) fn push_root(&mut self, root: impl Into<u64>) {
let id = root.into();
self.edits.push(PushRoot { root: id });
}
@ -214,18 +299,18 @@ impl<'a> Mutations<'a> {
self.edits.push(PopRoot {});
}
pub(crate) fn replace_with(&mut self, root: ElementId, m: u32) {
let root = root.as_u64();
pub(crate) fn replace_with(&mut self, root: impl Into<u64>, m: u32) {
let root = root.into();
self.edits.push(ReplaceWith { m, root });
}
pub(crate) fn insert_after(&mut self, root: ElementId, n: u32) {
let root = root.as_u64();
pub(crate) fn insert_after(&mut self, root: impl Into<u64>, n: u32) {
let root = root.into();
self.edits.push(InsertAfter { n, root });
}
pub(crate) fn insert_before(&mut self, root: ElementId, n: u32) {
let root = root.as_u64();
pub(crate) fn insert_before(&mut self, root: impl Into<u64>, n: u32) {
let root = root.into();
self.edits.push(InsertBefore { n, root });
}
@ -234,13 +319,13 @@ impl<'a> Mutations<'a> {
}
// Remove Nodes from the dom
pub(crate) fn remove(&mut self, id: u64) {
self.edits.push(Remove { root: id });
pub(crate) fn remove(&mut self, id: impl Into<u64>) {
self.edits.push(Remove { root: id.into() });
}
// Create
pub(crate) fn create_text_node(&mut self, text: &'a str, id: ElementId) {
let id = id.as_u64();
pub(crate) fn create_text_node(&mut self, text: &'a str, id: impl Into<u64>) {
let id = id.into();
self.edits.push(CreateTextNode { text, root: id });
}
@ -248,20 +333,68 @@ impl<'a> Mutations<'a> {
&mut self,
tag: &'static str,
ns: Option<&'static str>,
id: ElementId,
id: impl Into<u64>,
) {
let id = id.as_u64();
let id = id.into();
match ns {
Some(ns) => self.edits.push(CreateElementNs { root: id, ns, tag }),
None => self.edits.push(CreateElement { root: id, tag }),
}
}
// placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom
pub(crate) fn create_placeholder(&mut self, id: ElementId) {
let id = id.as_u64();
pub(crate) fn create_placeholder(&mut self, id: impl Into<u64>) {
let id = id.into();
self.edits.push(CreatePlaceholder { root: id });
}
// Create
pub(crate) fn create_text_node_template(
&mut self,
text: &'a str,
id: impl Into<u64>,
locally_static: bool,
) {
let id = id.into();
self.edits.push(CreateTextNodeTemplate {
text,
root: id,
locally_static,
});
}
pub(crate) fn create_element_template(
&mut self,
tag: &'static str,
ns: Option<&'static str>,
id: impl Into<u64>,
locally_static: bool,
fully_static: bool,
) {
let id = id.into();
match ns {
Some(ns) => self.edits.push(CreateElementNsTemplate {
root: id,
ns,
tag,
locally_static,
fully_static,
}),
None => self.edits.push(CreateElementTemplate {
root: id,
tag,
locally_static,
fully_static,
}),
}
}
// placeholders are nodes that don't get rendered but still exist as an "anchor" in the real dom
pub(crate) fn create_placeholder_template(&mut self, id: impl Into<u64>) {
let id = id.into();
self.edits.push(CreatePlaceholderTemplate { root: id });
}
// events
pub(crate) fn new_event_listener(&mut self, listener: &Listener, scope: ScopeId) {
let Listener {
@ -270,7 +403,13 @@ impl<'a> Mutations<'a> {
..
} = listener;
let element_id = mounted_node.get().unwrap().as_u64();
let element_id = match mounted_node.get().unwrap() {
GlobalNodeId::TemplateId {
template_ref_id: _,
template_node_id,
} => template_node_id.into(),
GlobalNodeId::VNodeId(id) => id.into(),
};
self.edits.push(NewEventListener {
scope,
@ -278,39 +417,41 @@ impl<'a> Mutations<'a> {
root: element_id,
});
}
pub(crate) fn remove_event_listener(&mut self, event: &'static str, root: u64) {
self.edits.push(RemoveEventListener { event, root });
pub(crate) fn remove_event_listener(&mut self, event: &'static str, root: impl Into<u64>) {
self.edits.push(RemoveEventListener {
event,
root: root.into(),
});
}
// modify
pub(crate) fn set_text(&mut self, text: &'a str, root: u64) {
pub(crate) fn set_text(&mut self, text: &'a str, root: impl Into<u64>) {
let root = root.into();
self.edits.push(SetText { text, root });
}
pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute<'a>, root: u64) {
pub(crate) fn set_attribute(&mut self, attribute: &'a Attribute<'a>, root: impl Into<u64>) {
let root = root.into();
let Attribute {
name,
value,
namespace,
..
value, attribute, ..
} = attribute;
self.edits.push(SetAttribute {
field: name,
field: attribute.name,
value: value.clone(),
ns: *namespace,
ns: attribute.namespace,
root,
});
}
pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: u64) {
let Attribute {
name, namespace, ..
} = attribute;
pub(crate) fn remove_attribute(&mut self, attribute: &Attribute, root: impl Into<u64>) {
let root = root.into();
let Attribute { attribute, .. } = attribute;
self.edits.push(RemoveAttribute {
name,
ns: *namespace,
name: attribute.name,
ns: attribute.namespace,
root,
});
}
@ -318,6 +459,33 @@ impl<'a> Mutations<'a> {
pub(crate) fn mark_dirty_scope(&mut self, scope: ScopeId) {
self.dirty_scopes.insert(scope);
}
pub(crate) fn create_templete(&mut self, id: impl Into<u64>) {
self.edits.push(CreateTemplate { id: id.into() });
}
pub(crate) fn finish_templete(&mut self, len: u32) {
self.edits.push(FinishTemplate { len });
}
pub(crate) fn create_template_ref(&mut self, id: impl Into<u64>, template_id: u64) {
self.edits.push(CreateTemplateRef {
id: id.into(),
template_id,
})
}
pub(crate) fn enter_template_ref(&mut self, id: impl Into<u64>) {
self.edits.push(EnterTemplateRef { root: id.into() });
}
pub(crate) fn exit_template_ref(&mut self) {
if let Some(&DomEdit::EnterTemplateRef { .. }) = self.edits.last() {
self.edits.pop();
} else {
self.edits.push(ExitTemplateRef {});
}
}
}
// refs are only assigned once

View file

@ -3,16 +3,70 @@
//! VNodes represent lazily-constructed VDom trees that support diffing and event handlers. These VNodes should be *very*
//! cheap and *very* fast to construct - building a full tree should be quick.
use crate::{
innerlude::{AttributeValue, ComponentPtr, Element, Properties, Scope, ScopeId, ScopeState},
dynamic_template_context::TemplateContext,
innerlude::{
AttributeValue, ComponentPtr, Element, Properties, Scope, ScopeId, ScopeState, Template,
TemplateId,
},
lazynodes::LazyNodes,
template::{TemplateNodeId, VTemplateRef},
AnyEvent, Component,
};
use bumpalo::{boxed::Box as BumpBox, Bump};
use std::{
cell::{Cell, RefCell},
fmt::{Arguments, Debug, Formatter},
num::ParseIntError,
rc::Rc,
str::FromStr,
};
/// The ID of a node in the vdom that is either standalone or in a template
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(untagged))]
pub enum GlobalNodeId {
/// The ID of a node and the template that contains it
TemplateId {
/// The template that contains the node
template_ref_id: ElementId,
/// The ID of the node in the template
template_node_id: TemplateNodeId,
},
/// The ID of a regular node
VNodeId(ElementId),
}
impl Into<GlobalNodeId> for ElementId {
fn into(self) -> GlobalNodeId {
GlobalNodeId::VNodeId(self)
}
}
impl PartialEq<ElementId> for GlobalNodeId {
fn eq(&self, other: &ElementId) -> bool {
match self {
GlobalNodeId::TemplateId { .. } => false,
GlobalNodeId::VNodeId(id) => id == other,
}
}
}
impl FromStr for GlobalNodeId {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some((tmpl_id, node_id)) = s.split_once(',') {
Ok(GlobalNodeId::TemplateId {
template_ref_id: ElementId(tmpl_id.parse()?),
template_node_id: TemplateNodeId(node_id.parse()?),
})
} else {
Ok(GlobalNodeId::VNodeId(ElementId(s.parse()?)))
}
}
}
/// A composable "VirtualNode" to declare a User Interface in the Dioxus VirtualDOM.
///
/// VNodes are designed to be lightweight and used with with a bump allocator. To create a VNode, you can use either of:
@ -112,6 +166,9 @@ pub enum VNode<'src> {
/// }
/// ```
Placeholder(&'src VPlaceholder),
/// Templetes ase generated by the rsx macro to eleminate diffing static nodes.
TemplateRef(&'src VTemplateRef<'src>),
}
impl<'src> VNode<'src> {
@ -123,6 +180,7 @@ impl<'src> VNode<'src> {
VNode::Fragment(f) => f.key,
VNode::Text(_t) => None,
VNode::Placeholder(_f) => None,
VNode::TemplateRef(t) => t.dynamic_context.key,
}
}
@ -143,6 +201,7 @@ impl<'src> VNode<'src> {
VNode::Placeholder(el) => el.id.get(),
VNode::Fragment(_) => None,
VNode::Component(_) => None,
VNode::TemplateRef(t) => t.id.get(),
}
}
@ -154,6 +213,7 @@ impl<'src> VNode<'src> {
VNode::Component(c) => VNode::Component(c),
VNode::Placeholder(a) => VNode::Placeholder(a),
VNode::Fragment(f) => VNode::Fragment(f),
VNode::TemplateRef(t) => VNode::TemplateRef(t),
}
}
}
@ -189,6 +249,11 @@ impl Debug for VNode<'_> {
.field("key", &comp.key)
.field("scope", &comp.scope)
.finish(),
VNode::TemplateRef(temp) => s
.debug_struct("VNode::TemplateRef")
.field("template_id", &temp.template_id)
.field("id", &temp.id)
.finish(),
}
}
}
@ -198,6 +263,8 @@ impl Debug for VNode<'_> {
/// `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.
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serialize", serde(transparent))]
pub struct ElementId(pub usize);
impl std::fmt::Display for ElementId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@ -206,13 +273,19 @@ impl std::fmt::Display for ElementId {
}
impl ElementId {
/// Convertt the ElementId to a `u64`.
pub fn as_u64(self) -> u64 {
/// Convert the ElementId to a `u64`.
pub fn as_u64(&self) -> u64 {
(*self).into()
}
}
impl Into<u64> for ElementId {
fn into(self) -> u64 {
self.0 as u64
}
}
fn empty_cell() -> Cell<Option<ElementId>> {
fn empty_cell<T>() -> Cell<Option<T>> {
Cell::new(None)
}
@ -267,7 +340,7 @@ pub struct VElement<'a> {
/// The parent of the Element (if any).
///
/// Used when bubbling events
pub parent: Cell<Option<ElementId>>,
pub parent: Cell<Option<GlobalNodeId>>,
/// The Listeners of the VElement.
pub listeners: &'a [Listener<'a>],
@ -330,30 +403,52 @@ pub trait DioxusElement {
}
}
type StaticStr = &'static str;
/// A discription of the attribute
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
#[cfg_attr(feature = "serialize", derive(serde::Serialize))]
#[cfg_attr(
all(feature = "serialize", any(feature = "hot-reload", debug_assertions)),
derive(serde::Deserialize)
)]
pub struct AttributeDiscription {
/// The name of the attribute.
#[cfg_attr(
all(feature = "serialize", any(feature = "hot-reload", debug_assertions)),
serde(deserialize_with = "crate::util::deserialize_static_leaky")
)]
pub name: StaticStr,
/// The namespace of the attribute.
///
/// Doesn't exist in the html spec.
/// Used in Dioxus to denote "style" tags and other attribute groups.
#[cfg_attr(
all(feature = "serialize", any(feature = "hot-reload", debug_assertions)),
serde(deserialize_with = "crate::util::deserialize_static_leaky_ns")
)]
pub namespace: Option<StaticStr>,
/// An indication of we should always try and set the attribute.
/// Used in controlled components to ensure changes are propagated.
pub volatile: bool,
}
/// An attribute on a DOM node, such as `id="my-thing"` or
/// `href="https://example.com"`.
#[derive(Clone, Debug)]
pub struct Attribute<'a> {
/// The name of the attribute.
pub name: &'static str,
/// The value of the attribute.
pub value: AttributeValue<'a>,
/// The discription of the attribute.
pub attribute: AttributeDiscription,
/// An indication if this attribute can be ignored during diffing
///
/// Usually only when there are no strings to be formatted (so the value is &'static str)
pub is_static: bool,
/// An indication of we should always try and set the attribute.
/// Used in controlled components to ensure changes are propagated.
pub is_volatile: bool,
/// 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 value of the attribute.
pub value: AttributeValue<'a>,
}
/// An event listener.
@ -361,7 +456,7 @@ pub struct Attribute<'a> {
pub struct Listener<'bump> {
/// The ID of the node that this listener is mounted to
/// Used to generate the event listener's ID on the DOM
pub mounted_node: Cell<Option<ElementId>>,
pub mounted_node: Cell<Option<GlobalNodeId>>,
/// The type of event to listen for.
///
@ -599,6 +694,20 @@ impl<'a> NodeFactory<'a> {
}))
}
/// Create a new [`Attribute`] from a attribute discrimination and a value
pub fn attr_disciption(
&self,
discription: AttributeDiscription,
val: Arguments,
) -> Attribute<'a> {
let (value, is_static) = self.raw_text(val);
Attribute {
attribute: discription,
is_static,
value: AttributeValue::Text(value),
}
}
/// Create a new [`Attribute`]
pub fn attr(
&self,
@ -609,11 +718,13 @@ impl<'a> NodeFactory<'a> {
) -> Attribute<'a> {
let (value, is_static) = self.raw_text(val);
Attribute {
name,
value: AttributeValue::Text(value),
attribute: AttributeDiscription {
name,
namespace,
volatile: is_volatile,
},
is_static,
namespace,
is_volatile,
value: AttributeValue::Text(value),
}
}
@ -627,11 +738,13 @@ impl<'a> NodeFactory<'a> {
is_static: bool,
) -> Attribute<'a> {
Attribute {
name,
value,
attribute: AttributeDiscription {
name,
namespace,
volatile: is_volatile,
},
is_static,
namespace,
is_volatile,
value,
}
}
@ -743,6 +856,27 @@ impl<'a> NodeFactory<'a> {
let callback = RefCell::new(Some(caller));
EventHandler { callback }
}
/// Create a refrence to a template
pub fn template_ref(
&self,
id: TemplateId,
template: Template,
dynamic_context: TemplateContext<'a>,
) -> VNode<'a> {
let borrow_ref = self.scope.templates.borrow();
// We only create the template if it doesn't already exist to allow for hot reloading
if !borrow_ref.contains_key(&id) {
drop(borrow_ref);
let mut borrow_mut = self.scope.templates.borrow_mut();
borrow_mut.insert(id.clone(), Rc::new(RefCell::new(template)));
}
VNode::TemplateRef(self.bump.alloc(VTemplateRef {
id: empty_cell(),
dynamic_context,
template_id: id,
}))
}
}
impl Debug for NodeFactory<'_> {

View file

@ -1,12 +1,19 @@
use crate::{innerlude::*, unsafe_utils::extend_vnode};
use crate::{
dynamic_template_context::TemplateContext,
innerlude::*,
template::{
TemplateAttribute, TemplateElement, TemplateNode, TemplateNodeId, TemplateNodeType,
TemplateValue, TextTemplateSegment,
},
unsafe_utils::extend_vnode,
};
use bumpalo::Bump;
use futures_channel::mpsc::UnboundedSender;
use fxhash::FxHashMap;
use slab::Slab;
use std::{
any::{Any, TypeId},
borrow::Borrow,
cell::{Cell, RefCell},
cell::{Cell, Ref, RefCell},
collections::{HashMap, HashSet},
future::Future,
pin::Pin,
@ -35,6 +42,10 @@ pub(crate) struct ScopeArena {
pub free_scopes: RefCell<Vec<*mut ScopeState>>,
pub nodes: RefCell<Slab<*const VNode<'static>>>,
pub tasks: Rc<TaskQueue>,
pub template_resolver: RefCell<TemplateResolver>,
pub templates: Rc<RefCell<FxHashMap<TemplateId, Rc<RefCell<Template>>>>>,
// this is used to store intermidiate artifacts of creating templates, so that the lifetime aligns with Mutations<'bump>.
pub template_bump: Bump,
}
impl ScopeArena {
@ -74,6 +85,9 @@ impl ScopeArena {
gen: Cell::new(0),
sender,
}),
template_resolver: RefCell::new(TemplateResolver::default()),
templates: Rc::new(RefCell::new(FxHashMap::default())),
template_bump: Bump::new(),
}
}
@ -93,8 +107,7 @@ impl ScopeArena {
fc_ptr: ComponentPtr,
vcomp: Box<dyn AnyProps>,
parent_scope: Option<ScopeId>,
container: ElementId,
subtree: u32,
container: GlobalNodeId,
) -> ScopeId {
// Increment the ScopeId system. ScopeIDs are never reused
let new_scope_id = ScopeId(self.scope_gen.get());
@ -124,7 +137,6 @@ impl ScopeArena {
scope.height = height;
scope.fnptr = fc_ptr;
scope.props.get_mut().replace(vcomp);
scope.subtree.set(subtree);
scope.frames[0].reset();
scope.frames[1].reset();
scope.shared_contexts.get_mut().clear();
@ -155,10 +167,6 @@ impl ScopeArena {
props: RefCell::new(Some(vcomp)),
frames: [BumpFrame::new(node_capacity), BumpFrame::new(node_capacity)],
// todo: subtrees
subtree: Cell::new(0),
is_subtree_root: Cell::default(),
generation: 0.into(),
tasks: self.tasks.clone(),
@ -172,6 +180,8 @@ impl ScopeArena {
hook_arena: Bump::new(),
hook_vals: RefCell::new(Vec::with_capacity(hook_capacity)),
hook_idx: Cell::default(),
templates: self.templates.clone(),
}),
);
}
@ -314,44 +324,140 @@ impl ScopeArena {
scope.cycle_frame();
}
pub fn call_listener_with_bubbling(&self, event: &UserEvent, element: ElementId) {
pub fn call_listener_with_bubbling(&self, event: &UserEvent, element: GlobalNodeId) {
let nodes = self.nodes.borrow();
let mut cur_el = Some(element);
let state = Rc::new(BubbleState::new());
while let Some(id) = cur_el.take() {
if let Some(el) = nodes.get(id.0) {
let real_el = unsafe { &**el };
if let VNode::Element(real_el) = real_el {
for listener in real_el.listeners.borrow().iter() {
if listener.event == event.name {
if state.canceled.get() {
// stop bubbling if canceled
return;
}
let mut cb = listener.callback.borrow_mut();
if let Some(cb) = cb.as_mut() {
// todo: arcs are pretty heavy to clone
// we really want to convert arc to rc
// unfortunately, the SchedulerMsg must be send/sync to be sent across threads
// we could convert arc to rc internally or something
(cb)(AnyEvent {
bubble_state: state.clone(),
data: event.data.clone(),
});
}
if !event.bubbles {
return;
}
if state.canceled.get() {
// stop bubbling if canceled
return;
}
match id {
GlobalNodeId::TemplateId {
template_ref_id,
template_node_id,
} => {
log::trace!(
"looking for listener in {:?} in node {:?}",
template_ref_id,
template_node_id
);
if let Some(template) = nodes.get(template_ref_id.0) {
let template = unsafe { &**template };
if let VNode::TemplateRef(template_ref) = template {
let templates = self.templates.borrow();
let template = templates.get(&template_ref.template_id).unwrap();
cur_el = template.borrow().with_node(
template_node_id,
bubble_template,
bubble_template,
(
&nodes,
&template_ref.dynamic_context,
&event,
&state,
template_ref_id,
),
);
}
}
cur_el = real_el.parent.get();
}
GlobalNodeId::VNodeId(id) => {
if let Some(el) = nodes.get(id.0) {
let real_el = unsafe { &**el };
log::trace!("looking for listener on {:?}", real_el);
if let VNode::Element(real_el) = real_el {
for listener in real_el.listeners.iter() {
if listener.event == event.name {
log::trace!("calling listener {:?}", listener.event);
let mut cb = listener.callback.borrow_mut();
if let Some(cb) = cb.as_mut() {
// todo: arcs are pretty heavy to clone
// we really want to convert arc to rc
// unfortunately, the SchedulerMsg must be send/sync to be sent across threads
// we could convert arc to rc internally or something
(cb)(AnyEvent {
bubble_state: state.clone(),
data: event.data.clone(),
});
}
break;
}
}
cur_el = real_el.parent.get();
}
}
}
}
if !event.bubbles {
return;
}
}
fn bubble_template<'b, Attributes, V, Children, Listeners, TextSegments, Text>(
node: &TemplateNode<Attributes, V, Children, Listeners, TextSegments, Text>,
ctx: (
&Ref<Slab<*const VNode>>,
&TemplateContext<'b>,
&UserEvent,
&Rc<BubbleState>,
ElementId,
),
) -> Option<GlobalNodeId>
where
Attributes: AsRef<[TemplateAttribute<V>]>,
V: TemplateValue,
Children: AsRef<[TemplateNodeId]>,
Listeners: AsRef<[usize]>,
TextSegments: AsRef<[TextTemplateSegment<Text>]>,
Text: AsRef<str>,
{
let (vnodes, dynamic_context, event, state, template_ref_id) = ctx;
if let TemplateNodeType::Element(el) = &node.node_type {
let TemplateElement { listeners, .. } = el;
for listener_idx in listeners.as_ref() {
let listener = dynamic_context.resolve_listener(*listener_idx);
if listener.event == event.name {
log::trace!("calling listener {:?}", listener.event);
let mut cb = listener.callback.borrow_mut();
if let Some(cb) = cb.as_mut() {
// todo: arcs are pretty heavy to clone
// we really want to convert arc to rc
// unfortunately, the SchedulerMsg must be send/sync to be sent across threads
// we could convert arc to rc internally or something
(cb)(AnyEvent {
bubble_state: state.clone(),
data: event.data.clone(),
});
}
break;
}
}
if let Some(id) = el.parent {
Some(GlobalNodeId::TemplateId {
template_ref_id,
template_node_id: id,
})
} else {
vnodes.get(template_ref_id.0).and_then(|el| {
let real_el = unsafe { &**el };
if let VNode::Element(real_el) = real_el {
real_el.parent.get()
} else {
None
}
})
}
} else {
None
}
}
}
@ -463,14 +569,10 @@ pub struct TaskId {
/// use case they might have.
pub struct ScopeState {
pub(crate) parent_scope: Option<*mut ScopeState>,
pub(crate) container: ElementId,
pub(crate) container: GlobalNodeId,
pub(crate) our_arena_idx: ScopeId,
pub(crate) height: u32,
pub(crate) fnptr: ComponentPtr,
// todo: subtrees
pub(crate) is_subtree_root: Cell<bool>,
pub(crate) subtree: Cell<u32>,
pub(crate) props: RefCell<Option<Box<dyn AnyProps>>>,
// nodes, items
@ -486,6 +588,9 @@ pub struct ScopeState {
// shared state -> todo: move this out of scopestate
pub(crate) shared_contexts: RefCell<HashMap<TypeId, Box<dyn Any>>>,
pub(crate) tasks: Rc<TaskQueue>,
// templates
pub(crate) templates: Rc<RefCell<FxHashMap<TemplateId, Rc<RefCell<Template>>>>>,
}
pub struct SelfReferentialItems<'a> {
@ -495,52 +600,6 @@ pub struct SelfReferentialItems<'a> {
// Public methods exposed to libraries and components
impl ScopeState {
/// Get the subtree ID that this scope belongs to.
///
/// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route
/// the mutations to the correct window/portal/subtree.
///
///
/// # Example
///
/// ```rust, ignore
/// let mut dom = VirtualDom::new(|cx| cx.render(rsx!{ div {} }));
/// dom.rebuild();
///
/// let base = dom.base_scope();
///
/// assert_eq!(base.subtree(), 0);
/// ```
///
/// todo: enable
pub(crate) fn _subtree(&self) -> u32 {
self.subtree.get()
}
/// Create a new subtree with this scope as the root of the subtree.
///
/// Each component has its own subtree ID - the root subtree has an ID of 0. This ID is used by the renderer to route
/// the mutations to the correct window/portal/subtree.
///
/// This method
///
/// # Example
///
/// ```rust, ignore
/// fn App(cx: Scope) -> Element {
/// render!(div { "Subtree {id}"})
/// };
/// ```
///
/// todo: enable subtree
pub(crate) fn _create_subtree(&self) -> Option<u32> {
if self.is_subtree_root.get() {
None
} else {
todo!()
}
}
/// Get the height of this Scope - IE the number of scopes above it.
///
/// A Scope with a height of `0` is the root scope - there are no other scopes above it.
@ -903,8 +962,6 @@ impl ScopeState {
self.hook_idx.set(0);
self.parent_scope = None;
self.generation.set(0);
self.is_subtree_root.set(false);
self.subtree.set(0);
// next: shared context data
self.shared_contexts.get_mut().clear();

File diff suppressed because it is too large Load diff

View file

@ -34,7 +34,10 @@ impl<'a> Iterator for ElementIdIterator<'a> {
if let Some((count, node)) = self.stack.last_mut() {
match node {
// We can only exit our looping when we get "real" nodes
VNode::Element(_) | VNode::Text(_) | VNode::Placeholder(_) => {
VNode::Element(_)
| VNode::Text(_)
| VNode::Placeholder(_)
| VNode::TemplateRef(_) => {
// We've recursed INTO an element/text
// We need to recurse *out* of it and move forward to the next
// println!("Found element! Returning it!");
@ -81,3 +84,44 @@ impl<'a> Iterator for ElementIdIterator<'a> {
returned_node
}
}
/// This intentionally leaks once per element name to allow more flexability when hot reloding templetes
#[cfg(all(any(feature = "hot-reload", debug_assertions), feature = "serde"))]
mod leaky {
use std::sync::Mutex;
use fxhash::FxHashSet;
use once_cell::sync::Lazy;
static STATIC_CACHE: Lazy<Mutex<FxHashSet<&'static str>>> =
Lazy::new(|| Mutex::new(FxHashSet::default()));
pub fn deserialize_static_leaky<'de, D>(d: D) -> Result<&'static str, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
let s = <&str>::deserialize(d)?;
Ok(if let Some(stat) = STATIC_CACHE.lock().unwrap().get(s) {
*stat
} else {
Box::leak(s.into())
})
}
pub fn deserialize_static_leaky_ns<'de, D>(d: D) -> Result<Option<&'static str>, D::Error>
where
D: serde::Deserializer<'de>,
{
use serde::Deserialize;
Ok(<Option<&str>>::deserialize(d)?.map(|s| {
if let Some(stat) = STATIC_CACHE.lock().unwrap().get(s) {
*stat
} else {
Box::leak(s.into())
}
}))
}
}
#[cfg(all(any(feature = "hot-reload", debug_assertions), feature = "serde"))]
pub use leaky::*;

View file

@ -128,6 +128,10 @@ pub enum SchedulerMsg {
/// Mark all components as dirty and update them
DirtyAll,
#[cfg(any(feature = "hot-reload", debug_assertions))]
/// Mark a template as dirty, used for hot reloading
SetTemplate(Box<SetTemplateMsg>),
/// New tasks from components that should be polled when the next poll is ready
NewTask(ScopeId),
}
@ -226,8 +230,7 @@ impl VirtualDom {
render_fn: root,
}),
None,
ElementId(0),
0,
GlobalNodeId::VNodeId(ElementId(0)),
);
Self {
@ -398,6 +401,25 @@ impl VirtualDom {
self.dirty_scopes.insert(*id);
}
}
#[cfg(any(feature = "hot-reload", debug_assertions))]
SchedulerMsg::SetTemplate(msg) => {
let SetTemplateMsg(id, tmpl) = *msg;
if self
.scopes
.templates
.borrow_mut()
.insert(
id.clone(),
std::rc::Rc::new(std::cell::RefCell::new(Template::Owned(tmpl))),
)
.is_some()
{
self.scopes.template_resolver.borrow_mut().mark_dirty(&id)
}
// mark any scopes that used the template as dirty
self.process_message(SchedulerMsg::DirtyAll);
}
}
}
@ -449,6 +471,7 @@ impl VirtualDom {
#[allow(unused)]
pub fn work_with_deadline(&mut self, mut deadline: impl FnMut() -> bool) -> Vec<Mutations> {
let mut committed_mutations = vec![];
self.scopes.template_bump.reset();
while !self.dirty_scopes.is_empty() {
let scopes = &self.scopes;
@ -524,11 +547,13 @@ impl VirtualDom {
/// ```
pub fn rebuild(&mut self) -> Mutations {
let scope_id = ScopeId(0);
let mut diff_state = DiffState::new(&self.scopes);
let mut diff_state = DiffState::new(&self.scopes);
self.scopes.run_scope(scope_id);
diff_state.element_stack.push(ElementId(0));
diff_state
.element_stack
.push(GlobalNodeId::VNodeId(ElementId(0)));
diff_state.scope_stack.push(scope_id);
let node = self.scopes.fin_head(scope_id);
@ -625,7 +650,9 @@ impl VirtualDom {
/// ```
pub fn diff_vnodes<'a>(&'a self, old: &'a VNode<'a>, new: &'a VNode<'a>) -> Mutations<'a> {
let mut machine = DiffState::new(&self.scopes);
machine.element_stack.push(ElementId(0));
machine
.element_stack
.push(GlobalNodeId::VNodeId(ElementId(0)));
machine.scope_stack.push(ScopeId(0));
machine.diff_node(old, new);
@ -648,7 +675,9 @@ impl VirtualDom {
pub fn create_vnodes<'a>(&'a self, nodes: LazyNodes<'a, '_>) -> Mutations<'a> {
let mut machine = DiffState::new(&self.scopes);
machine.scope_stack.push(ScopeId(0));
machine.element_stack.push(ElementId(0));
machine
.element_stack
.push(GlobalNodeId::VNodeId(ElementId(0)));
let node = self.render_vnodes(nodes);
let created = machine.create_node(node);
machine.mutations.append_children(created as u32);
@ -677,17 +706,32 @@ impl VirtualDom {
let mut create = DiffState::new(&self.scopes);
create.scope_stack.push(ScopeId(0));
create.element_stack.push(ElementId(0));
create
.element_stack
.push(GlobalNodeId::VNodeId(ElementId(0)));
let created = create.create_node(old);
create.mutations.append_children(created as u32);
let mut edit = DiffState::new(&self.scopes);
edit.scope_stack.push(ScopeId(0));
edit.element_stack.push(ElementId(0));
edit.element_stack.push(GlobalNodeId::VNodeId(ElementId(0)));
edit.diff_node(old, new);
(create.mutations, edit.mutations)
}
/// Runs a function with the template associated with a given id.
pub fn with_template<R>(&self, id: &TemplateId, mut f: impl FnMut(&Template) -> R) -> R {
self.scopes
.templates
.borrow()
.get(id)
.map(|inner| {
let borrow = &*inner;
f(&borrow.borrow())
})
.unwrap()
}
}
/*

View file

@ -15,7 +15,6 @@ keywords = ["dom", "ui", "gui", "react", "wasm"]
dioxus-core = { path = "../core", version = "^0.2.1", features = ["serialize"] }
dioxus-html = { path = "../html", features = ["serialize"], version = "^0.2.1" }
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1" }
dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true }
serde = "1.0.136"
serde_json = "1.0.79"
@ -33,7 +32,7 @@ webbrowser = "0.7.1"
infer = "0.9.0"
dunce = "1.0.2"
interprocess = { version = "1.1.1", optional = true }
interprocess = { version = "1.1.1" }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "0.9.3"
@ -44,7 +43,7 @@ tokio_runtime = ["tokio"]
fullscreen = ["wry/fullscreen"]
transparent = ["wry/transparent"]
tray = ["wry/tray"]
hot-reload = ["dioxus-rsx-interpreter", "interprocess"]
hot-reload = ["dioxus-core/hot-reload"]
[dev-dependencies]
dioxus-core-macro = { path = "../core-macro" }

View file

@ -51,7 +51,7 @@ impl DesktopController {
dom.base_scope().provide_context(window_context);
// allow other proccesses to send the new rsx text to the @dioxusin ipc channel and recieve erros on the @dioxusout channel
#[cfg(feature = "hot-reload")]
#[cfg(any(feature = "hot-reload", debug_assertions))]
crate::hot_reload::init(&dom);
let edits = dom.rebuild();

View file

@ -3,7 +3,7 @@
use std::any::Any;
use std::sync::Arc;
use dioxus_core::{ElementId, EventPriority, UserEvent};
use dioxus_core::{EventPriority, GlobalNodeId, UserEvent};
use dioxus_html::event_bubbles;
use dioxus_html::on::*;
use serde::{Deserialize, Serialize};
@ -37,7 +37,7 @@ pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
#[derive(Deserialize, Serialize)]
struct ImEvent {
event: String,
mounted_dom_id: u64,
mounted_dom_id: GlobalNodeId,
contents: serde_json::Value,
}
@ -48,7 +48,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
contents,
} = serde_json::from_value(val).unwrap();
let mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
let mounted_dom_id = Some(mounted_dom_id);
let name = event_name_from_type(&event);
let event = make_synthetic_event(&event, contents);

View file

@ -1,7 +1,6 @@
use dioxus_core::VirtualDom;
use dioxus_rsx_interpreter::{error::Error, ErrorHandler, SetManyRsxMessage, RSX_CONTEXT};
use dioxus_core::{SchedulerMsg, SetTemplateMsg, VirtualDom};
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
use std::io::{BufRead, BufReader, Write};
use std::io::{BufRead, BufReader};
use std::time::Duration;
use std::{sync::Arc, sync::Mutex};
@ -15,29 +14,6 @@ pub(crate) fn init(dom: &VirtualDom) {
let latest_in_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
Arc::new(Mutex::new(None));
let latest_in_connection_handle = latest_in_connection.clone();
let latest_out_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>> =
Arc::new(Mutex::new(None));
let latest_out_connection_handle = latest_out_connection.clone();
struct DesktopErrorHandler {
latest_connection: Arc<Mutex<Option<BufReader<LocalSocketStream>>>>,
}
impl ErrorHandler for DesktopErrorHandler {
fn handle_error(&self, err: Error) {
if let Some(conn) = &mut *self.latest_connection.lock().unwrap() {
conn.get_mut()
.write_all((serde_json::to_string(&err).unwrap() + "\n").as_bytes())
.unwrap();
} else {
panic!("{}", err);
}
}
}
RSX_CONTEXT.set_error_handler(DesktopErrorHandler {
latest_connection: latest_out_connection_handle,
});
RSX_CONTEXT.provide_scheduler_channel(dom.get_scheduler_channel());
// connect to processes for incoming data
std::thread::spawn(move || {
@ -48,14 +24,7 @@ pub(crate) fn init(dom: &VirtualDom) {
}
});
// connect to processes for outgoing errors
std::thread::spawn(move || {
if let Ok(listener) = LocalSocketListener::bind("@dioxusout") {
for conn in listener.incoming().filter_map(handle_error) {
*latest_out_connection.lock().unwrap() = Some(BufReader::new(conn));
}
}
});
let mut channel = dom.get_scheduler_channel();
std::thread::spawn(move || {
loop {
@ -63,8 +32,10 @@ pub(crate) fn init(dom: &VirtualDom) {
let mut buf = String::new();
match conn.read_line(&mut buf) {
Ok(_) => {
let msgs: SetManyRsxMessage = serde_json::from_str(&buf).unwrap();
RSX_CONTEXT.extend(msgs);
let msg: SetTemplateMsg = serde_json::from_str(&buf).unwrap();
channel
.start_send(SchedulerMsg::SetTemplate(Box::new(msg)))
.unwrap();
}
Err(err) => {
if err.kind() != std::io::ErrorKind::WouldBlock {

View file

@ -8,7 +8,7 @@ mod controller;
mod desktop_context;
mod escape;
mod events;
#[cfg(feature = "hot-reload")]
#[cfg(any(feature = "hot-reload", debug_assertions))]
mod hot_reload;
mod protocol;

View file

@ -17,12 +17,6 @@ dioxus-html = { path = "../html", version = "^0.2.1", optional = true }
dioxus-core-macro = { path = "../core-macro", version = "^0.2.1", optional = true }
dioxus-hooks = { path = "../hooks", version = "^0.2.1", optional = true }
dioxus-rsx = { path = "../rsx", optional = true }
dioxus-rsx-interpreter = { path = "../rsx_interpreter", optional = true }
# dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.2.1", optional = true }
# dioxus-native-core = { path = "./packages/native-core", version = "^0.2.0", optional = true }
# dioxus-native-core-macro = { path = "./packages/native-core-macro", version = "^0.2.0", optional = true }
[features]
default = ["macro", "hooks", "html"]
@ -30,8 +24,7 @@ macro = ["dioxus-core-macro", "dioxus-rsx"]
html = ["dioxus-html"]
hooks = ["dioxus-hooks"]
hot-reload = [
"dioxus-core-macro/hot-reload",
"dioxus-rsx-interpreter",
"dioxus-core/hot-reload",
]
[dev-dependencies]

View file

@ -33,10 +33,4 @@ pub mod prelude {
#[cfg(feature = "html")]
pub use dioxus_elements::{GlobalAttributes, SvgAttributes};
#[cfg(feature = "hot-reload")]
pub use dioxus_rsx_interpreter::{
captuered_context::{CapturedContext, FormattedArg, IfmtArgs},
get_line_num, resolve_scope, CodeLocation, RsxContext,
};
}

View file

@ -30,12 +30,29 @@ fn test_original_diff() {
assert_eq!(
mutations.edits,
[
CreateElement { root: 1, tag: "div" },
CreateElement { root: 2, tag: "div" },
CreateTextNode { root: 3, text: "Hello, world!" },
AppendChildren { many: 1 },
CreateTemplate { id: 0 },
CreateElementTemplate {
root: 4503599627370495,
tag: "div",
locally_static: true,
fully_static: true
},
CreateElementTemplate {
root: 4503599627370496,
tag: "div",
locally_static: true,
fully_static: true
},
CreateTextNodeTemplate {
root: 4503599627370497,
text: "Hello, world!",
locally_static: true
},
AppendChildren { many: 1 },
AppendChildren { many: 1 },
FinishTemplate { len: 1 },
CreateTemplateRef { id: 1, template_id: 0 },
AppendChildren { many: 1 }
]
);
}
@ -66,18 +83,49 @@ fn create() {
assert_eq!(
mutations.edits,
[
CreateElement { root: 1, tag: "div" },
CreateElement { root: 2, tag: "div" },
CreateTextNode { root: 3, text: "Hello, world!" },
CreateElement { root: 4, tag: "div" },
CreateElement { root: 5, tag: "div" },
CreateTextNode { root: 6, text: "hello" },
CreateTextNode { root: 7, text: "world" },
AppendChildren { many: 2 },
CreateTemplate { id: 0 },
CreateElementTemplate {
root: 4503599627370495,
tag: "div",
locally_static: true,
fully_static: false
},
CreateElementTemplate {
root: 4503599627370496,
tag: "div",
locally_static: true,
fully_static: false
},
CreateTextNodeTemplate {
root: 4503599627370497,
text: "Hello, world!",
locally_static: true
},
CreateElementTemplate {
root: 4503599627370498,
tag: "div",
locally_static: true,
fully_static: false
},
CreateElementTemplate {
root: 4503599627370499,
tag: "div",
locally_static: true,
fully_static: false
},
CreatePlaceholderTemplate { root: 4503599627370500 },
AppendChildren { many: 1 },
AppendChildren { many: 1 },
AppendChildren { many: 2 },
AppendChildren { many: 1 },
AppendChildren { many: 1 },
FinishTemplate { len: 1 },
CreateTemplateRef { id: 1, template_id: 0 },
EnterTemplateRef { root: 1 },
CreateTextNode { root: 2, text: "hello" },
CreateTextNode { root: 3, text: "world" },
ReplaceWith { root: 4503599627370500, m: 2 },
ExitTemplateRef {},
AppendChildren { many: 1 }
]
);
}
@ -99,16 +147,20 @@ fn create_list() {
assert_eq!(
mutations.edits,
[
CreateElement { root: 1, tag: "div" },
CreateTextNode { root: 2, text: "hello" },
CreateTemplate { id: 0 },
CreateElementTemplate {
root: 4503599627370495,
tag: "div",
locally_static: true,
fully_static: true
},
CreateTextNodeTemplate { root: 4503599627370496, text: "hello", locally_static: true },
AppendChildren { many: 1 },
CreateElement { root: 3, tag: "div" },
CreateTextNode { root: 4, text: "hello" },
AppendChildren { many: 1 },
CreateElement { root: 5, tag: "div" },
CreateTextNode { root: 6, text: "hello" },
AppendChildren { many: 1 },
AppendChildren { many: 3 },
FinishTemplate { len: 1 },
CreateTemplateRef { id: 1, template_id: 0 },
CreateTemplateRef { id: 2, template_id: 0 },
CreateTemplateRef { id: 3, template_id: 0 },
AppendChildren { many: 3 }
]
);
}
@ -131,11 +183,38 @@ fn create_simple() {
assert_eq!(
mutations.edits,
[
CreateElement { root: 1, tag: "div" },
CreateElement { root: 2, tag: "div" },
CreateElement { root: 3, tag: "div" },
CreateElement { root: 4, tag: "div" },
AppendChildren { many: 4 },
CreateTemplate { id: 0 },
CreateElementTemplate {
root: 4503599627370495,
tag: "div",
locally_static: true,
fully_static: true
},
AppendChildren { many: 0 },
CreateElementTemplate {
root: 4503599627370496,
tag: "div",
locally_static: true,
fully_static: true
},
AppendChildren { many: 0 },
CreateElementTemplate {
root: 4503599627370497,
tag: "div",
locally_static: true,
fully_static: true
},
AppendChildren { many: 0 },
CreateElementTemplate {
root: 4503599627370498,
tag: "div",
locally_static: true,
fully_static: true
},
AppendChildren { many: 0 },
FinishTemplate { len: 4 },
CreateTemplateRef { id: 1, template_id: 0 },
AppendChildren { many: 1 }
]
);
}
@ -168,25 +247,50 @@ fn create_components() {
assert_eq!(
mutations.edits,
[
CreateElement { root: 1, tag: "h1" },
CreateElement { root: 2, tag: "div" },
CreateTextNode { root: 3, text: "abc1" },
CreateTemplate { id: 0 },
CreateElementTemplate {
root: 4503599627370495,
tag: "h1",
locally_static: true,
fully_static: true
},
AppendChildren { many: 0 },
CreateElementTemplate {
root: 4503599627370496,
tag: "div",
locally_static: true,
fully_static: false
},
CreatePlaceholderTemplate { root: 4503599627370497 },
AppendChildren { many: 1 },
CreateElement { root: 4, tag: "p" },
CreateElement { root: 5, tag: "h1" },
CreateElement { root: 6, tag: "div" },
CreateTextNode { root: 7, text: "abc2" },
AppendChildren { many: 1 },
CreateElement { root: 8, tag: "p" },
CreateElement { root: 9, tag: "h1" },
CreateElement { root: 10, tag: "div" },
CreateTextNode { root: 11, text: "abc3" },
AppendChildren { many: 1 },
CreateElement { root: 12, tag: "p" },
AppendChildren { many: 9 },
CreateElementTemplate {
root: 4503599627370498,
tag: "p",
locally_static: true,
fully_static: true
},
AppendChildren { many: 0 },
FinishTemplate { len: 3 },
CreateTemplateRef { id: 1, template_id: 0 },
EnterTemplateRef { root: 1 },
CreateTextNode { root: 2, text: "abc1" },
ReplaceWith { root: 4503599627370497, m: 1 },
ExitTemplateRef {},
CreateTemplateRef { id: 3, template_id: 0 },
EnterTemplateRef { root: 3 },
CreateTextNode { root: 4, text: "abc2" },
ReplaceWith { root: 4503599627370497, m: 1 },
ExitTemplateRef {},
CreateTemplateRef { id: 5, template_id: 0 },
EnterTemplateRef { root: 5 },
CreateTextNode { root: 6, text: "abc3" },
ReplaceWith { root: 4503599627370497, m: 1 },
ExitTemplateRef {},
AppendChildren { many: 3 }
]
);
}
#[test]
fn anchors() {
static App: Component = |cx| {
@ -201,11 +305,19 @@ fn anchors() {
assert_eq!(
mutations.edits,
[
CreateElement { root: 1, tag: "div" },
CreateTextNode { root: 2, text: "hello" },
CreateTemplate { id: 0 },
CreateElementTemplate {
root: 4503599627370495,
tag: "div",
locally_static: true,
fully_static: true
},
CreateTextNodeTemplate { root: 4503599627370496, text: "hello", locally_static: true },
AppendChildren { many: 1 },
CreatePlaceholder { root: 3 },
AppendChildren { many: 2 },
FinishTemplate { len: 1 },
CreateTemplateRef { id: 1, template_id: 0 },
CreatePlaceholder { root: 2 },
AppendChildren { many: 2 }
]
);
}

View file

@ -6,10 +6,10 @@
//!
//! It does not validated that component lifecycles work properly. This is done in another test file.
use dioxus::prelude::*;
use dioxus::{core_macro::rsx_without_templates, prelude::*};
fn new_dom() -> VirtualDom {
VirtualDom::new(|cx| render!("hi"))
VirtualDom::new(|cx| cx.render(rsx_without_templates!("hi")))
}
use dioxus_core::DomEdit::*;
@ -19,8 +19,8 @@ use dioxus_core::DomEdit::*;
fn html_and_rsx_generate_the_same_output() {
let dom = new_dom();
let (create, change) = dom.diff_lazynodes(
rsx! ( div { "Hello world" } ),
rsx! ( div { "Goodbye world" } ),
rsx_without_templates! ( div { "Hello world" } ),
rsx_without_templates! ( div { "Goodbye world" } ),
);
assert_eq!(
create.edits,
@ -40,7 +40,7 @@ fn html_and_rsx_generate_the_same_output() {
fn fragments_create_properly() {
let dom = new_dom();
let create = dom.create_vnodes(rsx! {
let create = dom.create_vnodes(rsx_without_templates! {
div { "Hello a" }
div { "Hello b" }
div { "Hello c" }
@ -68,8 +68,8 @@ fn fragments_create_properly() {
fn empty_fragments_create_anchors() {
let dom = new_dom();
let left = rsx!({ (0..0).map(|_f| rsx! { div {}}) });
let right = rsx!({ (0..1).map(|_f| rsx! { div {}}) });
let left = rsx_without_templates!({ (0..0).map(|_f| rsx_without_templates! { div {}}) });
let right = rsx_without_templates!({ (0..1).map(|_f| rsx_without_templates! { div {}}) });
let (create, change) = dom.diff_lazynodes(left, right);
@ -91,8 +91,8 @@ fn empty_fragments_create_anchors() {
fn empty_fragments_create_many_anchors() {
let dom = new_dom();
let left = rsx!({ (0..0).map(|_f| rsx! { div {}}) });
let right = rsx!({ (0..5).map(|_f| rsx! { div {}}) });
let left = rsx_without_templates!({ (0..0).map(|_f| rsx_without_templates! { div {}}) });
let right = rsx_without_templates!({ (0..5).map(|_f| rsx_without_templates! { div {}}) });
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
@ -119,10 +119,10 @@ fn empty_fragments_create_many_anchors() {
fn empty_fragments_create_anchors_with_many_children() {
let dom = new_dom();
let left = rsx!({ (0..0).map(|_| rsx! { div {} }) });
let right = rsx!({
let left = rsx_without_templates!({ (0..0).map(|_| rsx_without_templates! { div {} }) });
let right = rsx_without_templates!({
(0..3).map(|f| {
rsx! { div { "hello: {f}" }}
rsx_without_templates! { div { "hello: {f}" }}
})
});
@ -154,12 +154,12 @@ fn empty_fragments_create_anchors_with_many_children() {
fn many_items_become_fragment() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
(0..2).map(|_| {
rsx! { div { "hello" }}
rsx_without_templates! { div { "hello" }}
})
});
let right = rsx!({ (0..0).map(|_| rsx! { div {} }) });
let right = rsx_without_templates!({ (0..0).map(|_| rsx_without_templates! { div {} }) });
let (create, change) = dom.diff_lazynodes(left, right);
assert_eq!(
@ -190,14 +190,14 @@ fn many_items_become_fragment() {
fn two_equal_fragments_are_equal() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
(0..2).map(|_| {
rsx! { div { "hello" }}
rsx_without_templates! { div { "hello" }}
})
});
let right = rsx!({
let right = rsx_without_templates!({
(0..2).map(|_| {
rsx! { div { "hello" }}
rsx_without_templates! { div { "hello" }}
})
});
@ -210,12 +210,12 @@ fn two_equal_fragments_are_equal() {
fn two_fragments_with_differrent_elements_are_differet() {
let dom = new_dom();
let left = rsx!(
{ (0..2).map(|_| rsx! { div { }} ) }
let left = rsx_without_templates!(
{ (0..2).map(|_| rsx_without_templates! { div { }} ) }
p {}
);
let right = rsx!(
{ (0..5).map(|_| rsx! (h1 { }) ) }
let right = rsx_without_templates!(
{ (0..5).map(|_| rsx_without_templates! (h1 { }) ) }
p {}
);
@ -243,12 +243,12 @@ fn two_fragments_with_differrent_elements_are_differet() {
fn two_fragments_with_differrent_elements_are_differet_shorter() {
let dom = new_dom();
let left = rsx!(
{(0..5).map(|f| {rsx! { div { }}})}
let left = rsx_without_templates!(
{(0..5).map(|f| {rsx_without_templates! { div { }}})}
p {}
);
let right = rsx!(
{(0..2).map(|f| {rsx! { h1 { }}})}
let right = rsx_without_templates!(
{(0..2).map(|f| {rsx_without_templates! { h1 { }}})}
p {}
);
@ -287,12 +287,12 @@ fn two_fragments_with_differrent_elements_are_differet_shorter() {
fn two_fragments_with_same_elements_are_differet() {
let dom = new_dom();
let left = rsx!(
{(0..2).map(|f| {rsx! { div { }}})}
let left = rsx_without_templates!(
{(0..2).map(|f| rsx_without_templates! { div { }})}
p {}
);
let right = rsx!(
{(0..5).map(|f| {rsx! { div { }}})}
let right = rsx_without_templates!(
{(0..5).map(|f| rsx_without_templates! { div { }})}
p {}
);
@ -322,12 +322,12 @@ fn two_fragments_with_same_elements_are_differet() {
fn keyed_diffing_order() {
let dom = new_dom();
let left = rsx!(
{(0..5).map(|f| {rsx! { div { key: "{f}" }}})}
let left = rsx_without_templates!(
{(0..5).map(|f| {rsx_without_templates! { div { key: "{f}" }}})}
p {"e"}
);
let right = rsx!(
{(0..2).map(|f| rsx! { div { key: "{f}" }})}
let right = rsx_without_templates!(
{(0..2).map(|f| rsx_without_templates! { div { key: "{f}" }})}
p {"e"}
);
@ -343,15 +343,15 @@ fn keyed_diffing_order() {
fn keyed_diffing_out_of_order() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
[0, 1, 2, 3, /**/ 4, 5, 6, /**/ 7, 8, 9].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx!({
let right = rsx_without_templates!({
[0, 1, 2, 3, /**/ 6, 4, 5, /**/ 7, 8, 9].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
@ -368,15 +368,15 @@ fn keyed_diffing_out_of_order() {
fn keyed_diffing_out_of_order_adds() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx!({
let right = rsx_without_templates!({
[/**/ 8, 7, 4, 5, 6 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
@ -395,15 +395,15 @@ fn keyed_diffing_out_of_order_adds() {
fn keyed_diffing_out_of_order_adds_2() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx!({
let right = rsx_without_templates!({
[/**/ 7, 8, 4, 5, 6 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
@ -423,15 +423,15 @@ fn keyed_diffing_out_of_order_adds_2() {
fn keyed_diffing_out_of_order_adds_3() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx!({
let right = rsx_without_templates!({
[/**/ 4, 8, 7, 5, 6 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
@ -451,15 +451,15 @@ fn keyed_diffing_out_of_order_adds_3() {
fn keyed_diffing_out_of_order_adds_4() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx!({
let right = rsx_without_templates!({
[/**/ 4, 5, 8, 7, 6 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
@ -479,15 +479,15 @@ fn keyed_diffing_out_of_order_adds_4() {
fn keyed_diffing_out_of_order_adds_5() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx!({
let right = rsx_without_templates!({
[/**/ 4, 5, 6, 8, 7 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
@ -502,15 +502,15 @@ fn keyed_diffing_out_of_order_adds_5() {
fn keyed_diffing_additions() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
[/**/ 4, 5, 6, 7, 8 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx!({
let right = rsx_without_templates!({
[/**/ 4, 5, 6, 7, 8, 9, 10 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
@ -529,15 +529,15 @@ fn keyed_diffing_additions() {
fn keyed_diffing_additions_and_moves_on_ends() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
[/**/ 4, 5, 6, 7 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx!({
let right = rsx_without_templates!({
[/**/ 7, 4, 5, 6, 11, 12 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
@ -561,15 +561,15 @@ fn keyed_diffing_additions_and_moves_on_ends() {
fn keyed_diffing_additions_and_moves_in_middle() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
[/**/ 1, 2, 3, 4 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx!({
let right = rsx_without_templates!({
[/**/ 4, 1, 7, 8, 2, 5, 6, 3 /**/].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
@ -598,15 +598,15 @@ fn keyed_diffing_additions_and_moves_in_middle() {
fn controlled_keyed_diffing_out_of_order() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
[4, 5, 6, 7].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx!({
let right = rsx_without_templates!({
[0, 5, 9, 6, 4].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
@ -636,15 +636,15 @@ fn controlled_keyed_diffing_out_of_order() {
fn controlled_keyed_diffing_out_of_order_max_test() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
[0, 1, 2, 3, 4].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
let right = rsx!({
let right = rsx_without_templates!({
[3, 0, 1, 10, 2].iter().map(|f| {
rsx! { div { key: "{f}" }}
rsx_without_templates! { div { key: "{f}" }}
})
});
@ -668,15 +668,15 @@ fn controlled_keyed_diffing_out_of_order_max_test() {
fn remove_list() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
(0..10).rev().take(5).map(|i| {
rsx! { Fragment { key: "{i}", "{i}" }}
rsx_without_templates! { Fragment { key: "{i}", "{i}" }}
})
});
let right = rsx!({
let right = rsx_without_templates!({
(0..10).rev().take(2).map(|i| {
rsx! { Fragment { key: "{i}", "{i}" }}
rsx_without_templates! { Fragment { key: "{i}", "{i}" }}
})
});
@ -702,15 +702,15 @@ fn remove_list() {
fn remove_list_nokeyed() {
let dom = new_dom();
let left = rsx!({
let left = rsx_without_templates!({
(0..10).rev().take(5).map(|i| {
rsx! { Fragment { "{i}" }}
rsx_without_templates! { Fragment { "{i}" }}
})
});
let right = rsx!({
let right = rsx_without_templates!({
(0..10).rev().take(2).map(|i| {
rsx! { Fragment { "{i}" }}
rsx_without_templates! { Fragment { "{i}" }}
})
});
@ -732,10 +732,10 @@ fn add_nested_elements() {
let vdom = new_dom();
let (_create, change) = vdom.diff_lazynodes(
rsx! {
rsx_without_templates! {
div{}
},
rsx! {
rsx_without_templates! {
div{
div{}
}
@ -758,10 +758,10 @@ fn add_listeners() {
let vdom = new_dom();
let (_create, change) = vdom.diff_lazynodes(
rsx! {
rsx_without_templates! {
div{}
},
rsx! {
rsx_without_templates! {
div{
onkeyup: |_| {},
onkeydown: |_| {},
@ -783,13 +783,13 @@ fn remove_listeners() {
let vdom = new_dom();
let (_create, change) = vdom.diff_lazynodes(
rsx! {
rsx_without_templates! {
div{
onkeyup: |_| {},
onkeydown: |_| {},
}
},
rsx! {
rsx_without_templates! {
div{}
},
);
@ -808,12 +808,12 @@ fn diff_listeners() {
let vdom = new_dom();
let (_create, change) = vdom.diff_lazynodes(
rsx! {
rsx_without_templates! {
div{
onkeydown: |_| {},
}
},
rsx! {
rsx_without_templates! {
div{
onkeyup: |_| {},
}

View file

@ -37,27 +37,37 @@ fn test_early_abort() {
assert_eq!(
edits.edits,
[
CreateElement { tag: "div", root: 1 },
CreateTextNode { text: "Hello, world!", root: 2 },
AppendChildren { many: 1 },
CreateTemplate { id: 0 },
CreateElementTemplate {
root: 4503599627370495,
tag: "div",
locally_static: true,
fully_static: true
},
CreateTextNodeTemplate {
root: 4503599627370496,
text: "Hello, world!",
locally_static: true
},
AppendChildren { many: 1 },
FinishTemplate { len: 1 },
CreateTemplateRef { id: 1, template_id: 0 },
AppendChildren { many: 1 }
]
);
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
[CreatePlaceholder { root: 3 }, ReplaceWith { root: 1, m: 1 },],
[CreatePlaceholder { root: 2 }, ReplaceWith { root: 1, m: 1 },],
);
let edits = dom.hard_diff(ScopeId(0));
assert_eq!(
edits.edits,
[
CreateElement { tag: "div", root: 1 }, // keys get reused
CreateTextNode { text: "Hello, world!", root: 2 }, // keys get reused
AppendChildren { many: 1 },
ReplaceWith { root: 3, m: 1 },
CreateTemplateRef { id: 1, template_id: 0 }, // gets reused
ReplaceWith { root: 2, m: 1 }
]
);
}

View file

@ -2,7 +2,7 @@
#![allow(non_snake_case)]
//! Tests for the lifecycle of components.
use dioxus::prelude::*;
use dioxus::{core_macro::rsx_without_templates, prelude::*};
use dioxus_core::DomEdit::*;
use std::sync::{Arc, Mutex};
@ -16,7 +16,7 @@ fn manual_diffing() {
static App: Component<AppProps> = |cx| {
let val = cx.props.value.lock().unwrap();
cx.render(rsx! { div { "{val}" } })
cx.render(rsx_without_templates! { div { "{val}" } })
};
let value = Arc::new(Mutex::new("Hello"));
@ -38,7 +38,7 @@ fn events_generate() {
let inner = match *count {
0 => {
rsx! {
rsx_without_templates! {
div {
onclick: move |_| *count += 1,
div {
@ -81,21 +81,21 @@ fn components_generate() {
*render_phase += 1;
cx.render(match *render_phase {
1 => rsx!("Text0"),
2 => rsx!(div {}),
3 => rsx!("Text2"),
4 => rsx!(Child {}),
5 => rsx!({ None as Option<()> }),
6 => rsx!("text 3"),
7 => rsx!({ (0..2).map(|f| rsx!("text {f}")) }),
8 => rsx!(Child {}),
1 => rsx_without_templates!("Text0"),
2 => rsx_without_templates!(div {}),
3 => rsx_without_templates!("Text2"),
4 => rsx_without_templates!(Child {}),
5 => rsx_without_templates!({ None as Option<()> }),
6 => rsx_without_templates!("text 3"),
7 => rsx_without_templates!({ (0..2).map(|f| rsx_without_templates!("text {f}")) }),
8 => rsx_without_templates!(Child {}),
_ => todo!(),
})
};
fn Child(cx: Scope) -> Element {
println!("Running child");
cx.render(rsx! {
cx.render(rsx_without_templates! {
h1 {}
})
}
@ -175,53 +175,53 @@ fn component_swap() {
*render_phase += 1;
cx.render(match *render_phase {
0 => rsx!(
0 => rsx_without_templates!(
div {
NavBar {}
Dashboard {}
}
),
1 => rsx!(
1 => rsx_without_templates!(
div {
NavBar {}
Results {}
}
),
2 => rsx!(
2 => rsx_without_templates!(
div {
NavBar {}
Dashboard {}
}
),
3 => rsx!(
3 => rsx_without_templates!(
div {
NavBar {}
Results {}
}
),
4 => rsx!(
4 => rsx_without_templates!(
div {
NavBar {}
Dashboard {}
}
),
_ => rsx!("blah"),
_ => rsx_without_templates!("blah"),
})
};
static NavBar: Component = |cx| {
println!("running navbar");
cx.render(rsx! {
cx.render(rsx_without_templates! {
h1 {
"NavBar"
{(0..3).map(|f| rsx!(NavLink {}))}
{(0..3).map(|f| rsx_without_templates!(NavLink {}))}
}
})
};
static NavLink: Component = |cx| {
println!("running navlink");
cx.render(rsx! {
cx.render(rsx_without_templates! {
h1 {
"NavLink"
}
@ -230,7 +230,7 @@ fn component_swap() {
static Dashboard: Component = |cx| {
println!("running dashboard");
cx.render(rsx! {
cx.render(rsx_without_templates! {
div {
"dashboard"
}
@ -239,7 +239,7 @@ fn component_swap() {
static Results: Component = |cx| {
println!("running results");
cx.render(rsx! {
cx.render(rsx_without_templates! {
div {
"results"
}

View file

@ -1,6 +1,6 @@
#![allow(unused, non_upper_case_globals, non_snake_case)]
use dioxus::prelude::*;
use dioxus::{core_macro::rsx_without_templates, prelude::*};
use dioxus_core::{DomEdit, Mutations, SchedulerMsg, ScopeId};
use std::rc::Rc;
use DomEdit::*;
@ -11,12 +11,12 @@ fn shared_state_test() {
static App: Component = |cx| {
cx.provide_context(Rc::new(MySharedState("world!")));
cx.render(rsx!(Child {}))
cx.render(rsx_without_templates!(Child {}))
};
static Child: Component = |cx| {
let shared = cx.consume_context::<Rc<MySharedState>>()?;
cx.render(rsx!("Hello, {shared.0}"))
cx.render(rsx_without_templates!("Hello, {shared.0}"))
};
let mut dom = VirtualDom::new(App);
@ -42,13 +42,13 @@ fn swap_test() {
cx.provide_context(Rc::new(MySharedState("world!")));
let child = match *val % 2 {
0 => rsx!(
0 => rsx_without_templates!(
Child1 {
Child1 { }
Child2 { }
}
),
_ => rsx!(
_ => rsx_without_templates!(
Child2 {
Child2 { }
Child2 { }
@ -56,7 +56,7 @@ fn swap_test() {
),
};
cx.render(rsx!(
cx.render(rsx_without_templates!(
Router {
div { child }
}
@ -65,14 +65,14 @@ fn swap_test() {
#[inline_props]
fn Router<'a>(cx: Scope, children: Element<'a>) -> Element<'a> {
cx.render(rsx!(div { children }))
cx.render(rsx_without_templates!(div { children }))
}
#[inline_props]
fn Child1<'a>(cx: Scope, children: Element<'a>) -> Element {
let shared = cx.consume_context::<Rc<MySharedState>>().unwrap();
println!("Child1: {}", shared.0);
cx.render(rsx! {
cx.render(rsx_without_templates! {
div {
"{shared.0}",
children
@ -84,7 +84,7 @@ fn swap_test() {
fn Child2<'a>(cx: Scope, children: Element<'a>) -> Element {
let shared = cx.consume_context::<Rc<MySharedState>>().unwrap();
println!("Child2: {}", shared.0);
cx.render(rsx! {
cx.render(rsx_without_templates! {
h1 {
"{shared.0}",
children

View file

@ -63,14 +63,38 @@ fn conditional_rendering() {
assert_eq!(
mutations.edits,
[
CreateElement { root: 1, tag: "h1" },
CreateTextNode { root: 2, text: "hello" },
CreateTemplate { id: 0 },
CreateElementTemplate {
root: 4503599627370495,
tag: "h1",
locally_static: true,
fully_static: true
},
CreateTextNodeTemplate { root: 4503599627370496, text: "hello", locally_static: true },
AppendChildren { many: 1 },
CreateElement { root: 3, tag: "span" },
CreateTextNode { root: 4, text: "a" },
CreatePlaceholderTemplate { root: 4503599627370497 },
CreatePlaceholderTemplate { root: 4503599627370498 },
FinishTemplate { len: 3 },
CreateTemplateRef { id: 1, template_id: 0 },
EnterTemplateRef { root: 1 },
CreateTemplate { id: 1 },
CreateElementTemplate {
root: 4503599627370495,
tag: "span",
locally_static: true,
fully_static: true
},
CreateTextNodeTemplate { root: 4503599627370496, text: "a", locally_static: true },
AppendChildren { many: 1 },
CreatePlaceholder { root: 5 },
AppendChildren { many: 3 },
FinishTemplate { len: 1 },
CreateTemplateRef { id: 2, template_id: 1 },
ReplaceWith { root: 4503599627370497, m: 1 },
ExitTemplateRef {},
EnterTemplateRef { root: 1 },
CreatePlaceholder { root: 3 },
ReplaceWith { root: 4503599627370498, m: 1 },
ExitTemplateRef {},
AppendChildren { many: 1 }
]
)
}

View file

@ -1,6 +1,5 @@
use crate::{GlobalAttributes, SvgAttributes};
use dioxus_core::*;
use std::fmt::Arguments;
macro_rules! builder_constructors {
(
@ -28,10 +27,12 @@ macro_rules! builder_constructors {
impl $name {
$(
$(#[$attr_method])*
pub fn $fil<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr(stringify!($fil), val, None, false)
}
#[allow(non_upper_case_globals)]
pub const $fil: AttributeDiscription = AttributeDiscription{
name: stringify!($fil),
namespace: None,
volatile: false
};
)*
}
)*
@ -57,9 +58,13 @@ macro_rules! builder_constructors {
impl $name {
$(
pub fn $fil<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr(stringify!($fil), val, Some(stringify!($namespace)), false)
}
#[allow(non_upper_case_globals)]
pub const $fil: AttributeDiscription = AttributeDiscription{
name: stringify!($fil),
namespace: Some(stringify!($namespace)),
volatile: false
};
)*
}
)*
@ -1061,13 +1066,19 @@ impl input {
/// - `time`
/// - `url`
/// - `week`
pub fn r#type<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("type", val, None, false)
}
#[allow(non_upper_case_globals)]
pub const r#type: AttributeDiscription = AttributeDiscription {
name: "type",
namespace: None,
volatile: false,
};
pub fn value<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("value", val, None, true)
}
#[allow(non_upper_case_globals)]
pub const value: AttributeDiscription = AttributeDiscription {
name: "value",
namespace: None,
volatile: true,
};
}
/*
@ -1077,41 +1088,63 @@ volatile attributes
impl script {
// r#async: Bool,
// r#type: String, // TODO could be an enum
pub fn r#type<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("type", val, None, false)
}
pub fn r#script<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("script", val, None, false)
}
#[allow(non_upper_case_globals)]
pub const r#type: AttributeDiscription = AttributeDiscription {
name: "type",
namespace: None,
volatile: false,
};
#[allow(non_upper_case_globals)]
pub const r#script: AttributeDiscription = AttributeDiscription {
name: "script",
namespace: None,
volatile: false,
};
}
impl button {
pub fn r#type<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("type", val, None, false)
}
#[allow(non_upper_case_globals)]
pub const r#type: AttributeDiscription = AttributeDiscription {
name: "type",
namespace: None,
volatile: false,
};
}
impl select {
pub fn value<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("value", val, None, true)
}
#[allow(non_upper_case_globals)]
pub const value: AttributeDiscription = AttributeDiscription {
name: "value",
namespace: None,
volatile: true,
};
}
impl option {
pub fn selected<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("selected", val, None, true)
}
#[allow(non_upper_case_globals)]
pub const selected: AttributeDiscription = AttributeDiscription {
name: "selected",
namespace: None,
volatile: true,
};
}
impl textarea {
pub fn value<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("value", val, None, true)
}
#[allow(non_upper_case_globals)]
pub const value: AttributeDiscription = AttributeDiscription {
name: "value",
namespace: None,
volatile: true,
};
}
impl label {
pub fn r#for<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("for", val, None, false)
}
#[allow(non_upper_case_globals)]
pub const r#for: AttributeDiscription = AttributeDiscription {
name: "for",
namespace: None,
volatile: false,
};
}
builder_constructors! {

View file

@ -1,5 +1,4 @@
use dioxus_core::*;
use std::fmt::Arguments;
macro_rules! no_namespace_trait_methods {
(
@ -9,10 +8,12 @@ macro_rules! no_namespace_trait_methods {
)*
) => {
$(
$(#[$attr])*
fn $name<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr(stringify!($name), val, None, false)
}
#[allow(non_upper_case_globals)]
const $name: AttributeDiscription = AttributeDiscription{
name: stringify!($name),
namespace: None,
volatile: false
};
)*
};
}
@ -24,11 +25,12 @@ macro_rules! style_trait_methods {
)*
) => {
$(
#[inline]
$(#[$attr])*
fn $name<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr($lit, val, Some("style"), false)
}
#[allow(non_upper_case_globals)]
const $name: AttributeDiscription = AttributeDiscription{
name: $lit,
namespace: Some("style"),
volatile: false
};
)*
};
}
@ -40,10 +42,12 @@ macro_rules! aria_trait_methods {
)*
) => {
$(
$(#[$attr])*
fn $name<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr($lit, val, None, false)
}
#[allow(non_upper_case_globals)]
const $name: AttributeDiscription = AttributeDiscription{
name: $lit,
namespace: None,
volatile: false
};
)*
};
}
@ -53,9 +57,12 @@ pub trait GlobalAttributes {
///
/// For more information, see the MDN docs:
/// <https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault>
fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("dioxus-prevent-default", val, None, false)
}
#[allow(non_upper_case_globals)]
const prevent_default: AttributeDiscription = AttributeDiscription {
name: "dioxus-prevent-default",
namespace: None,
volatile: false,
};
no_namespace_trait_methods! {
accesskey;
@ -597,9 +604,12 @@ pub trait SvgAttributes {
///
/// For more information, see the MDN docs:
/// <https://developer.mozilla.org/en-US/docs/Web/API/Event/preventDefault>
fn prevent_default<'a>(&self, cx: NodeFactory<'a>, val: Arguments) -> Attribute<'a> {
cx.attr("dioxus-prevent-default", val, None, false)
}
#[allow(non_upper_case_globals)]
const prevent_default: AttributeDiscription = AttributeDiscription {
name: "dioxus-prevent-default",
namespace: None,
volatile: false,
};
aria_trait_methods! {
accent_height: "accent-height",
accumulate: "accumulate",

View file

@ -73,4 +73,44 @@ extern "C" {
#[wasm_bindgen(method)]
pub fn RemoveAttribute(this: &Interpreter, root: u64, field: &str, ns: Option<&str>);
#[wasm_bindgen(method)]
pub fn CreateTemplateRef(this: &Interpreter, id: u64, template_id: u64);
#[wasm_bindgen(method)]
pub fn CreateTemplate(this: &Interpreter, id: u64);
#[wasm_bindgen(method)]
pub fn FinishTemplate(this: &Interpreter, len: u32);
#[wasm_bindgen(method)]
pub fn EnterTemplateRef(this: &Interpreter, id: u64);
#[wasm_bindgen(method)]
pub fn ExitTemplateRef(this: &Interpreter);
#[wasm_bindgen(method)]
pub fn CreateElementTemplate(
this: &Interpreter,
tag: &str,
root: u64,
locally_static: bool,
fully_static: bool,
);
#[wasm_bindgen(method)]
pub fn CreateElementNsTemplate(
this: &Interpreter,
tag: &str,
id: u64,
ns: &str,
locally_static: bool,
fully_static: bool,
);
#[wasm_bindgen(method)]
pub fn CreateTextNodeTemplate(this: &Interpreter, text: &str, root: u64, locally_static: bool);
#[wasm_bindgen(method)]
pub fn CreatePlaceholderTemplate(this: &Interpreter, root: u64);
}

View file

@ -1,3 +1,7 @@
// id > Number.MAX_SAFE_INTEGER/2 in template ref
// id <= Number.MAX_SAFE_INTEGER/2 in global nodes
const templateIdLimit = BigInt((Number.MAX_SAFE_INTEGER - 1) / 2);
export function main() {
let root = window.document.getElementById("main");
if (root != null) {
@ -6,6 +10,123 @@ export function main() {
}
}
class TemplateRef {
constructor(fragment, dynamicNodePaths, roots, id) {
this.fragment = fragment;
this.dynamicNodePaths = dynamicNodePaths;
this.roots = roots;
this.id = id;
this.placed = false;
this.nodes = [];
}
build(id) {
if (!this.nodes[id]) {
let current = this.fragment;
const path = this.dynamicNodePaths[id];
for (let i = 0; i < path.length; i++) {
const idx = path[i];
current = current.firstChild;
for (let i2 = 0; i2 < idx; i2++) {
current = current.nextSibling;
}
}
this.nodes[id] = current;
}
}
get(id) {
this.build(id);
return this.nodes[id];
}
parent() {
return this.roots[0].parentNode;
}
first() {
return this.roots[0];
}
last() {
return this.roots[this.roots.length - 1];
}
move() {
// move the root nodes into a new template
this.fragment = new DocumentFragment();
for (let n of this.roots) {
this.fragment.appendChild(n);
}
}
getFragment() {
if (!this.placed) {
this.placed = true;
}
else {
this.move();
}
return this.fragment;
}
}
class Template {
constructor(template_id, id) {
this.nodes = [];
this.dynamicNodePaths = [];
this.template_id = template_id;
this.id = id;
this.template = document.createElement("template");
this.reconstructingRefrencesIndex = null;
}
finalize(roots) {
for (let i = 0; i < roots.length; i++) {
let node = roots[i];
let path = [i];
const is_element = node.nodeType == 1;
const locally_static = is_element && !node.hasAttribute("data-dioxus-dynamic");
if (!locally_static) {
this.dynamicNodePaths[node.tmplId] = [...path];
}
const traverse_children = is_element && !node.hasAttribute("data-dioxus-fully-static");
if (traverse_children) {
this.createIds(path, node);
}
this.template.content.appendChild(node);
}
document.head.appendChild(this.template);
}
createIds(path, root) {
let i = 0;
for (let node = root.firstChild; node != null; node = node.nextSibling) {
let new_path = [...path, i];
const is_element = node.nodeType == 1;
const locally_static = is_element && !node.hasAttribute("data-dioxus-dynamic");
if (!locally_static) {
this.dynamicNodePaths[node.tmplId] = [...new_path];
}
const traverse_children = is_element && !node.hasAttribute("data-dioxus-fully-static");
if (traverse_children) {
this.createIds(new_path, node);
}
i++;
}
}
ref(id) {
const template = this.template.content.cloneNode(true);
let roots = [];
this.reconstructingRefrencesIndex = 0;
for (let node = template.firstChild; node != null; node = node.nextSibling) {
roots.push(node);
}
return new TemplateRef(template, this.dynamicNodePaths, roots, id);
}
}
class ListenerMap {
constructor(root) {
// bubbling events can listen at the root element
@ -53,21 +174,18 @@ class ListenerMap {
element.removeEventListener(event_name, handler);
}
}
removeAllNonBubbling(element) {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id];
}
}
export class Interpreter {
constructor(root) {
this.root = root;
this.stack = [root];
this.templateInProgress = null;
this.insideTemplateRef = [];
this.listeners = new ListenerMap(root);
this.handlers = {};
this.lastNodeWasText = false;
this.nodes = [root];
this.templates = [];
}
top() {
return this.stack[this.stack.length - 1];
@ -75,11 +193,45 @@ export class Interpreter {
pop() {
return this.stack.pop();
}
currentTemplateId() {
if (this.insideTemplateRef.length) {
return this.insideTemplateRef[this.insideTemplateRef.length - 1].id;
}
else {
return null;
}
}
getId(id) {
if (this.templateInProgress !== null) {
return this.templates[this.templateInProgress].nodes[id - templateIdLimit];
}
else if (this.insideTemplateRef.length && id >= templateIdLimit) {
return this.insideTemplateRef[this.insideTemplateRef.length - 1].get(id - templateIdLimit);
}
else {
return this.nodes[id];
}
}
SetNode(id, node) {
this.nodes[id] = node;
if (this.templateInProgress !== null) {
id -= templateIdLimit;
node.tmplId = id;
this.templates[this.templateInProgress].nodes[id] = node;
}
else if (this.insideTemplateRef.length && id >= templateIdLimit) {
id -= templateIdLimit;
let last = this.insideTemplateRef[this.insideTemplateRef.length - 1];
last.childNodes[id] = node;
if (last.nodeCache[id]) {
last.nodeCache[id] = node;
}
}
else {
this.nodes[id] = node;
}
}
PushRoot(root) {
const node = this.nodes[root];
const node = this.getId(root);
this.stack.push(node);
}
PopRoot() {
@ -89,73 +241,126 @@ export class Interpreter {
let root = this.stack[this.stack.length - (1 + many)];
let to_add = this.stack.splice(this.stack.length - many);
for (let i = 0; i < many; i++) {
root.appendChild(to_add[i]);
const child = to_add[i];
if (child instanceof TemplateRef) {
root.appendChild(child.getFragment());
}
else {
root.appendChild(child);
}
}
}
ReplaceWith(root_id, m) {
let root = this.nodes[root_id];
let els = this.stack.splice(this.stack.length - m);
if (is_element_node(root.nodeType)) {
this.listeners.removeAllNonBubbling(root);
let root = this.getId(root_id);
if (root instanceof TemplateRef) {
this.InsertBefore(root_id, m);
this.Remove(root_id);
}
else {
let els = this.stack.splice(this.stack.length - m).map(function (el) {
if (el instanceof TemplateRef) {
return el.getFragment();
}
else {
return el;
}
});
root.replaceWith(...els);
}
root.replaceWith(...els);
}
InsertAfter(root, n) {
let old = this.nodes[root];
let new_nodes = this.stack.splice(this.stack.length - n);
old.after(...new_nodes);
const old = this.getId(root);
const new_nodes = this.stack.splice(this.stack.length - n).map(function (el) {
if (el instanceof TemplateRef) {
return el.getFragment();
}
else {
return el;
}
});
if (old instanceof TemplateRef) {
const last = old.last();
last.after(...new_nodes);
}
else {
old.after(...new_nodes);
}
}
InsertBefore(root, n) {
let old = this.nodes[root];
let new_nodes = this.stack.splice(this.stack.length - n);
old.before(...new_nodes);
const old = this.getId(root);
const new_nodes = this.stack.splice(this.stack.length - n).map(function (el) {
if (el instanceof TemplateRef) {
return el.getFragment();
}
else {
return el;
}
});
if (old instanceof TemplateRef) {
const first = old.first();
first.before(...new_nodes);
}
else {
old.before(...new_nodes);
}
}
Remove(root) {
let node = this.nodes[root];
let node = this.getId(root);
if (node !== undefined) {
if (is_element_node(node)) {
this.listeners.removeAllNonBubbling(node);
if (node instanceof TemplateRef) {
for (let child of node.roots) {
child.remove();
}
}
else {
node.remove();
}
node.remove();
}
}
CreateTextNode(text, root) {
const node = document.createTextNode(text);
this.nodes[root] = node;
this.stack.push(node);
this.SetNode(root, node);
}
CreateElement(tag, root) {
const el = document.createElement(tag);
this.nodes[root] = el;
this.stack.push(el);
this.SetNode(root, el);
}
CreateElementNs(tag, root, ns) {
let el = document.createElementNS(ns, tag);
this.stack.push(el);
this.nodes[root] = el;
this.SetNode(root, el);
}
CreatePlaceholder(root) {
let el = document.createElement("pre");
el.hidden = true;
this.stack.push(el);
this.nodes[root] = el;
this.SetNode(root, el);
}
NewEventListener(event_name, root, handler, bubbles) {
const element = this.nodes[root];
element.setAttribute("data-dioxus-id", `${root}`);
const element = this.getId(root);
if (root >= templateIdLimit) {
let currentTemplateRefId = this.currentTemplateId();
root -= templateIdLimit;
element.setAttribute("data-dioxus-id", `${currentTemplateRefId},${root}`);
}
else {
element.setAttribute("data-dioxus-id", `${root}`);
}
this.listeners.create(event_name, element, handler, bubbles);
}
RemoveEventListener(root, event_name, bubbles) {
const element = this.nodes[root];
const element = this.getId(root);
element.removeAttribute(`data-dioxus-id`);
this.listeners.remove(element, event_name, bubbles);
}
SetText(root, text) {
this.nodes[root].textContent = text;
this.getId(root).data = text;
}
SetAttribute(root, field, value, ns) {
const name = field;
const node = this.nodes[root];
const node = this.getId(root);
if (ns === "style") {
// @ts-ignore
node.style[name] = value;
@ -189,7 +394,7 @@ export class Interpreter {
}
RemoveAttribute(root, field, ns) {
const name = field;
const node = this.nodes[root];
const node = this.getId(root);
if (ns == "style") {
node.style.removeProperty(name);
} else if (ns !== null || ns !== undefined) {
@ -206,52 +411,99 @@ export class Interpreter {
node.removeAttribute(name);
}
}
CreateTemplateRef(id, template_id) {
const el = this.templates[template_id].ref(id);
this.nodes[id] = el;
this.stack.push(el);
}
CreateTemplate(template_id) {
this.templateInProgress = template_id;
this.templates[template_id] = new Template(template_id, 0);
}
FinishTemplate(many) {
this.templates[this.templateInProgress].finalize(this.stack.splice(this.stack.length - many));
this.templateInProgress = null;
}
EnterTemplateRef(id) {
this.insideTemplateRef.push(this.nodes[id]);
}
ExitTemplateRef() {
this.insideTemplateRef.pop();
}
handleEdits(edits) {
this.stack.push(this.root);
for (let edit of edits) {
this.handleEdit(edit);
}
}
CreateElementTemplate(tag, root, locally_static, fully_static) {
const el = document.createElement(tag);
this.stack.push(el);
this.SetNode(root, el);
if (!locally_static)
el.setAttribute("data-dioxus-dynamic", "true");
if (fully_static)
el.setAttribute("data-dioxus-fully-static", fully_static);
}
CreateElementNsTemplate(tag, root, ns, locally_static, fully_static) {
const el = document.createElementNS(ns, tag);
this.stack.push(el);
this.SetNode(root, el);
if (!locally_static)
el.setAttribute("data-dioxus-dynamic", "true");
if (fully_static)
el.setAttribute("data-dioxus-fully-static", fully_static);
}
CreateTextNodeTemplate(text, root, locally_static) {
const node = document.createTextNode(text);
this.stack.push(node);
this.SetNode(root, node);
}
CreatePlaceholderTemplate(root) {
const el = document.createElement("pre");
el.setAttribute("data-dioxus-dynamic", "true");
el.hidden = true;
this.stack.push(el);
this.SetNode(root, el);
}
handleEdit(edit) {
switch (edit.type) {
case "PushRoot":
this.PushRoot(edit.root);
this.PushRoot(BigInt(edit.root));
break;
case "AppendChildren":
this.AppendChildren(edit.many);
break;
case "ReplaceWith":
this.ReplaceWith(edit.root, edit.m);
this.ReplaceWith(BigInt(edit.root), edit.m);
break;
case "InsertAfter":
this.InsertAfter(edit.root, edit.n);
this.InsertAfter(BigInt(edit.root), edit.n);
break;
case "InsertBefore":
this.InsertBefore(edit.root, edit.n);
this.InsertBefore(BigInt(edit.root), edit.n);
break;
case "Remove":
this.Remove(edit.root);
this.Remove(BigInt(edit.root));
break;
case "CreateTextNode":
this.CreateTextNode(edit.text, edit.root);
this.CreateTextNode(edit.text, BigInt(edit.root));
break;
case "CreateElement":
this.CreateElement(edit.tag, edit.root);
this.CreateElement(edit.tag, BigInt(edit.root));
break;
case "CreateElementNs":
this.CreateElementNs(edit.tag, edit.root, edit.ns);
this.CreateElementNs(edit.tag, BigInt(edit.root), edit.ns);
break;
case "CreatePlaceholder":
this.CreatePlaceholder(edit.root);
this.CreatePlaceholder(BigInt(edit.root));
break;
case "RemoveEventListener":
this.RemoveEventListener(edit.root, edit.event_name);
this.RemoveEventListener(BigInt(edit.root), edit.event_name);
break;
case "NewEventListener":
// this handler is only provided on desktop implementations since this
// method is not used by the web implementation
let handler = (event) => {
let target = event.target;
if (target != null) {
let realId = target.getAttribute(`data-dioxus-id`);
@ -330,26 +582,66 @@ export class Interpreter {
if (realId === null) {
return;
}
if (realId.includes(",")) {
realId = realId.split(',');
realId = {
template_ref_id: parseInt(realId[0]),
template_node_id: parseInt(realId[1]),
};
}
else {
realId = parseInt(realId);
}
window.ipc.postMessage(
serializeIpcMessage("user_event", {
event: edit.event_name,
mounted_dom_id: parseInt(realId),
mounted_dom_id: realId,
contents: contents,
})
);
}
};
this.NewEventListener(edit.event_name, edit.root, handler, event_bubbles(edit.event_name));
this.NewEventListener(edit.event_name, BigInt(edit.root), handler, event_bubbles(edit.event_name));
break;
case "SetText":
this.SetText(edit.root, edit.text);
this.SetText(BigInt(edit.root), edit.text);
break;
case "SetAttribute":
this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
this.SetAttribute(BigInt(edit.root), edit.field, edit.value, edit.ns);
break;
case "RemoveAttribute":
this.RemoveAttribute(edit.root, edit.name, edit.ns);
this.RemoveAttribute(BigInt(edit.root), edit.name, edit.ns);
break;
case "PopRoot":
this.PopRoot();
break;
case "CreateTemplateRef":
this.CreateTemplateRef(BigInt(edit.id), edit.template_id);
break;
case "CreateTemplate":
this.CreateTemplate(BigInt(edit.id));
break;
case "FinishTemplate":
this.FinishTemplate(edit.len);
break;
case "EnterTemplateRef":
this.EnterTemplateRef(BigInt(edit.root));
break;
case "ExitTemplateRef":
this.ExitTemplateRef();
break;
case "CreateElementTemplate":
this.CreateElementTemplate(edit.tag, BigInt(edit.root), edit.locally_static, edit.fully_static);
break;
case "CreateElementNsTemplate":
this.CreateElementNsTemplate(edit.tag, BigInt(edit.root), edit.ns, edit.locally_static, edit.fully_static);
break;
case "CreateTextNodeTemplate":
this.CreateTextNodeTemplate(edit.text, BigInt(edit.root), edit.locally_static);
break;
case "CreatePlaceholderTemplate":
this.CreatePlaceholderTemplate(BigInt(edit.root));
break;
}
}

View file

@ -5,7 +5,8 @@
use std::any::Any;
use std::sync::Arc;
use dioxus_core::{ElementId, EventPriority, UserEvent};
use dioxus_core::GlobalNodeId;
use dioxus_core::{EventPriority, UserEvent};
use dioxus_html::event_bubbles;
use dioxus_html::on::*;
@ -25,7 +26,7 @@ pub(crate) fn parse_ipc_message(payload: &str) -> Option<IpcMessage> {
#[derive(serde::Serialize, serde::Deserialize)]
struct ImEvent {
event: String,
mounted_dom_id: u64,
mounted_dom_id: GlobalNodeId,
contents: serde_json::Value,
}
@ -36,7 +37,7 @@ pub fn trigger_from_serialized(val: serde_json::Value) -> UserEvent {
contents,
} = serde_json::from_value(val).unwrap();
let mounted_dom_id = Some(ElementId(mounted_dom_id as usize));
let mounted_dom_id = Some(mounted_dom_id);
let name = event_name_from_type(&event);
let event = make_synthetic_event(&event, contents);

View file

@ -35,14 +35,186 @@ class IPC {
}
}
// id > Number.MAX_SAFE_INTEGER/2 in template ref
// id <= Number.MAX_SAFE_INTEGER/2 in global nodes
const templateIdLimit = BigInt((Number.MAX_SAFE_INTEGER - 1) / 2);
class TemplateRef {
constructor(fragment, dynamicNodePaths, roots, id) {
this.fragment = fragment;
this.dynamicNodePaths = dynamicNodePaths;
this.roots = roots;
this.id = id;
this.placed = false;
this.nodes = [];
}
build(id) {
if (!this.nodes[id]) {
let current = this.fragment;
const path = this.dynamicNodePaths[id];
for (let i = 0; i < path.length; i++) {
const idx = path[i];
current = current.firstChild;
for (let i2 = 0; i2 < idx; i2++) {
current = current.nextSibling;
}
}
this.nodes[id] = current;
}
}
get(id) {
this.build(id);
return this.nodes[id];
}
parent() {
return this.roots[0].parentNode;
}
first() {
return this.roots[0];
}
last() {
return this.roots[this.roots.length - 1];
}
move() {
// move the root nodes into a new template
this.fragment = new DocumentFragment();
for (let n of this.roots) {
this.fragment.appendChild(n);
}
}
getFragment() {
if (!this.placed) {
this.placed = true;
}
else {
this.move();
}
return this.fragment;
}
}
class Template {
constructor(template_id, id) {
this.nodes = [];
this.dynamicNodePaths = [];
this.template_id = template_id;
this.id = id;
this.template = document.createElement("template");
this.reconstructingRefrencesIndex = null;
}
finalize(roots) {
for (let i = 0; i < roots.length; i++) {
let node = roots[i];
let path = [i];
const is_element = node.nodeType == 1;
const locally_static = is_element && !node.hasAttribute("data-dioxus-dynamic");
if (!locally_static) {
this.dynamicNodePaths[node.tmplId] = [...path];
}
const traverse_children = is_element && !node.hasAttribute("data-dioxus-fully-static");
if (traverse_children) {
this.createIds(path, node);
}
this.template.content.appendChild(node);
}
document.head.appendChild(this.template);
}
createIds(path, root) {
let i = 0;
for (let node = root.firstChild; node != null; node = node.nextSibling) {
let new_path = [...path, i];
const is_element = node.nodeType == 1;
const locally_static = is_element && !node.hasAttribute("data-dioxus-dynamic");
if (!locally_static) {
this.dynamicNodePaths[node.tmplId] = [...new_path];
}
const traverse_children = is_element && !node.hasAttribute("data-dioxus-fully-static");
if (traverse_children) {
this.createIds(new_path, node);
}
i++;
}
}
ref(id) {
const template = this.template.content.cloneNode(true);
let roots = [];
this.reconstructingRefrencesIndex = 0;
for (let node = template.firstChild; node != null; node = node.nextSibling) {
roots.push(node);
}
return new TemplateRef(template, this.dynamicNodePaths, roots, id);
}
}
class ListenerMap {
constructor(root) {
// bubbling events can listen at the root element
this.global = {};
// non bubbling events listen at the element the listener was created at
this.local = {};
this.root = root;
}
create(event_name, element, handler, bubbles) {
if (bubbles) {
if (this.global[event_name] === undefined) {
this.global[event_name] = {};
this.global[event_name].active = 1;
this.global[event_name].callback = handler;
this.root.addEventListener(event_name, handler);
} else {
this.global[event_name].active++;
}
}
else {
const id = element.getAttribute("data-dioxus-id");
if (!this.local[id]) {
this.local[id] = {};
}
this.local[id][event_name] = handler;
element.addEventListener(event_name, handler);
}
}
remove(element, event_name, bubbles) {
if (bubbles) {
this.global[event_name].active--;
if (this.global[event_name].active === 0) {
this.root.removeEventListener(event_name, this.global[event_name].callback);
delete this.global[event_name];
}
}
else {
const id = element.getAttribute("data-dioxus-id");
delete this.local[id][event_name];
if (this.local[id].length === 0) {
delete this.local[id];
}
element.removeEventListener(event_name, handler);
}
}
}
class Interpreter {
constructor(root) {
this.root = root;
this.stack = [root];
this.listeners = {};
this.templateInProgress = null;
this.insideTemplateRef = [];
this.listeners = new ListenerMap(root);
this.handlers = {};
this.lastNodeWasText = false;
this.nodes = [root];
this.templates = [];
}
top() {
return this.stack[this.stack.length - 1];
@ -50,88 +222,174 @@ class Interpreter {
pop() {
return this.stack.pop();
}
currentTemplateId() {
if (this.insideTemplateRef.length) {
return this.insideTemplateRef[this.insideTemplateRef.length - 1].id;
}
else {
return null;
}
}
getId(id) {
if (this.templateInProgress !== null) {
return this.templates[this.templateInProgress].nodes[id - templateIdLimit];
}
else if (this.insideTemplateRef.length && id >= templateIdLimit) {
return this.insideTemplateRef[this.insideTemplateRef.length - 1].get(id - templateIdLimit);
}
else {
return this.nodes[id];
}
}
SetNode(id, node) {
if (this.templateInProgress !== null) {
id -= templateIdLimit;
node.tmplId = id;
this.templates[this.templateInProgress].nodes[id] = node;
}
else if (this.insideTemplateRef.length && id >= templateIdLimit) {
id -= templateIdLimit;
let last = this.insideTemplateRef[this.insideTemplateRef.length - 1];
last.childNodes[id] = node;
if (last.nodeCache[id]) {
last.nodeCache[id] = node;
}
}
else {
this.nodes[id] = node;
}
}
PushRoot(root) {
const node = this.nodes[root];
const node = this.getId(root);
this.stack.push(node);
}
PopRoot() {
this.stack.pop();
}
AppendChildren(many) {
let root = this.stack[this.stack.length - (1 + many)];
let to_add = this.stack.splice(this.stack.length - many);
for (let i = 0; i < many; i++) {
root.appendChild(to_add[i]);
const child = to_add[i];
if (child instanceof TemplateRef) {
root.appendChild(child.getFragment());
}
else {
root.appendChild(child);
}
}
}
ReplaceWith(root_id, m) {
let root = this.nodes[root_id];
let els = this.stack.splice(this.stack.length - m);
root.replaceWith(...els);
let root = this.getId(root_id);
if (root instanceof TemplateRef) {
this.InsertBefore(root_id, m);
this.Remove(root_id);
}
else {
let els = this.stack.splice(this.stack.length - m).map(function (el) {
if (el instanceof TemplateRef) {
return el.getFragment();
}
else {
return el;
}
});
root.replaceWith(...els);
}
}
InsertAfter(root, n) {
let old = this.nodes[root];
let new_nodes = this.stack.splice(this.stack.length - n);
old.after(...new_nodes);
const old = this.getId(root);
const new_nodes = this.stack.splice(this.stack.length - n).map(function (el) {
if (el instanceof TemplateRef) {
return el.getFragment();
}
else {
return el;
}
});
if (old instanceof TemplateRef) {
const last = old.last();
last.after(...new_nodes);
}
else {
old.after(...new_nodes);
}
}
InsertBefore(root, n) {
let old = this.nodes[root];
let new_nodes = this.stack.splice(this.stack.length - n);
old.before(...new_nodes);
const old = this.getId(root);
const new_nodes = this.stack.splice(this.stack.length - n).map(function (el) {
if (el instanceof TemplateRef) {
return el.getFragment();
}
else {
return el;
}
});
if (old instanceof TemplateRef) {
const first = old.first();
first.before(...new_nodes);
}
else {
old.before(...new_nodes);
}
}
Remove(root) {
let node = this.nodes[root];
let node = this.getId(root);
if (node !== undefined) {
node.remove();
if (node instanceof TemplateRef) {
for (let child of node.roots) {
child.remove();
}
}
else {
node.remove();
}
}
}
CreateTextNode(text, root) {
// todo: make it so the types are okay
const node = document.createTextNode(text);
this.nodes[root] = node;
this.stack.push(node);
this.SetNode(root, node);
}
CreateElement(tag, root) {
const el = document.createElement(tag);
// el.setAttribute("data-dioxus-id", `${root}`);
this.nodes[root] = el;
this.stack.push(el);
this.SetNode(root, el);
}
CreateElementNs(tag, root, ns) {
let el = document.createElementNS(ns, tag);
this.stack.push(el);
this.nodes[root] = el;
this.SetNode(root, el);
}
CreatePlaceholder(root) {
let el = document.createElement("pre");
el.hidden = true;
this.stack.push(el);
this.nodes[root] = el;
this.SetNode(root, el);
}
NewEventListener(event_name, root, handler) {
const element = this.nodes[root];
element.setAttribute("data-dioxus-id", `${root}`);
if (this.listeners[event_name] === undefined) {
this.listeners[event_name] = 0;
this.handlers[event_name] = handler;
this.root.addEventListener(event_name, handler);
} else {
this.listeners[event_name]++;
NewEventListener(event_name, root, handler, bubbles) {
const element = this.getId(root);
if (root >= templateIdLimit) {
let currentTemplateRefId = this.currentTemplateId();
root -= templateIdLimit;
element.setAttribute("data-dioxus-id", `${currentTemplateRefId},${root}`);
}
else {
element.setAttribute("data-dioxus-id", `${root}`);
}
this.listeners.create(event_name, element, handler, bubbles);
}
RemoveEventListener(root, event_name) {
const element = this.nodes[root];
RemoveEventListener(root, event_name, bubbles) {
const element = this.getId(root);
element.removeAttribute(`data-dioxus-id`);
this.listeners[event_name]--;
if (this.listeners[event_name] === 0) {
this.root.removeEventListener(event_name, this.handlers[event_name]);
delete this.listeners[event_name];
delete this.handlers[event_name];
}
this.listeners.remove(element, event_name, bubbles);
}
SetText(root, text) {
this.nodes[root].textContent = text;
this.getId(root).data = text;
}
SetAttribute(root, field, value, ns) {
const name = field;
const node = this.nodes[root];
const node = this.getId(root);
if (ns === "style") {
// @ts-ignore
node.style[name] = value;
@ -163,10 +421,14 @@ class Interpreter {
}
}
}
RemoveAttribute(root, name) {
const node = this.nodes[root];
if (name === "value") {
RemoveAttribute(root, field, ns) {
const name = field;
const node = this.getId(root);
if (ns == "style") {
node.style.removeProperty(name);
} else if (ns !== null || ns !== undefined) {
node.removeAttributeNS(ns, name);
} else if (name === "value") {
node.value = "";
} else if (name === "checked") {
node.checked = false;
@ -178,46 +440,94 @@ class Interpreter {
node.removeAttribute(name);
}
}
CreateTemplateRef(id, template_id) {
const el = this.templates[template_id].ref(id);
this.nodes[id] = el;
this.stack.push(el);
}
CreateTemplate(template_id) {
this.templateInProgress = template_id;
this.templates[template_id] = new Template(template_id, 0);
}
FinishTemplate(many) {
this.templates[this.templateInProgress].finalize(this.stack.splice(this.stack.length - many));
this.templateInProgress = null;
}
EnterTemplateRef(id) {
this.insideTemplateRef.push(this.nodes[id]);
}
ExitTemplateRef() {
this.insideTemplateRef.pop();
}
handleEdits(edits) {
this.stack.push(this.root);
for (let edit of edits) {
this.handleEdit(edit);
}
}
CreateElementTemplate(tag, root, locally_static, fully_static) {
const el = document.createElement(tag);
this.stack.push(el);
this.SetNode(root, el);
if (!locally_static)
el.setAttribute("data-dioxus-dynamic", "true");
if (fully_static)
el.setAttribute("data-dioxus-fully-static", fully_static);
}
CreateElementNsTemplate(tag, root, ns, locally_static, fully_static) {
const el = document.createElementNS(ns, tag);
this.stack.push(el);
this.SetNode(root, el);
if (!locally_static)
el.setAttribute("data-dioxus-dynamic", "true");
if (fully_static)
el.setAttribute("data-dioxus-fully-static", fully_static);
}
CreateTextNodeTemplate(text, root, locally_static) {
const node = document.createTextNode(text);
this.stack.push(node);
this.SetNode(root, node);
}
CreatePlaceholderTemplate(root) {
const el = document.createElement("pre");
el.setAttribute("data-dioxus-dynamic", "true");
el.hidden = true;
this.stack.push(el);
this.SetNode(root, el);
}
handleEdit(edit) {
switch (edit.type) {
case "PushRoot":
this.PushRoot(edit.root);
this.PushRoot(BigInt(edit.root));
break;
case "AppendChildren":
this.AppendChildren(edit.many);
break;
case "ReplaceWith":
this.ReplaceWith(edit.root, edit.m);
this.ReplaceWith(BigInt(edit.root), edit.m);
break;
case "InsertAfter":
this.InsertAfter(edit.root, edit.n);
this.InsertAfter(BigInt(edit.root), edit.n);
break;
case "InsertBefore":
this.InsertBefore(edit.root, edit.n);
this.InsertBefore(BigInt(edit.root), edit.n);
break;
case "Remove":
this.Remove(edit.root);
this.Remove(BigInt(edit.root));
break;
case "CreateTextNode":
this.CreateTextNode(edit.text, edit.root);
this.CreateTextNode(edit.text, BigInt(edit.root));
break;
case "CreateElement":
this.CreateElement(edit.tag, edit.root);
this.CreateElement(edit.tag, BigInt(edit.root));
break;
case "CreateElementNs":
this.CreateElementNs(edit.tag, edit.root, edit.ns);
this.CreateElementNs(edit.tag, BigInt(edit.root), edit.ns);
break;
case "CreatePlaceholder":
this.CreatePlaceholder(edit.root);
this.CreatePlaceholder(BigInt(edit.root));
break;
case "RemoveEventListener":
this.RemoveEventListener(edit.root, edit.event_name);
this.RemoveEventListener(BigInt(edit.root), edit.event_name);
break;
case "NewEventListener":
// this handler is only provided on desktop implementations since this
@ -245,7 +555,7 @@ class Interpreter {
}
// also prevent buttons from submitting
if (target.tagName === "BUTTON") {
if (target.tagName === "BUTTON" && event.type == "submit") {
event.preventDefault();
}
}
@ -269,11 +579,15 @@ class Interpreter {
if (shouldPreventDefault === `on${event.type}`) {
event.preventDefault();
}
if (event.type === "submit") {
event.preventDefault();
}
if (target.tagName === "FORM") {
if (
target.tagName === "FORM" &&
(event.type === "submit" || event.type === "input")
) {
for (let x = 0; x < target.elements.length; x++) {
let element = target.elements[x];
let name = element.getAttribute("name");
@ -281,6 +595,10 @@ class Interpreter {
if (element.getAttribute("type") === "checkbox") {
// @ts-ignore
contents.values[name] = element.checked ? "true" : "false";
} else if (element.getAttribute("type") === "radio") {
if (element.checked) {
contents.values[name] = element.value;
}
} else {
// @ts-ignore
contents.values[name] =
@ -290,28 +608,69 @@ class Interpreter {
}
}
if (realId == null) {
if (realId === null) {
return;
}
if (realId.includes(",")) {
realId = realId.split(',');
realId = {
template_ref_id: parseInt(realId[0]),
template_node_id: parseInt(realId[1]),
};
}
else {
realId = parseInt(realId);
}
window.ipc.send(
serializeIpcMessage("user_event", {
event: edit.event_name,
mounted_dom_id: parseInt(realId),
mounted_dom_id: realId,
contents: contents,
})
);
}
};
this.NewEventListener(edit.event_name, edit.root, handler);
this.NewEventListener(edit.event_name, BigInt(edit.root), handler, event_bubbles(edit.event_name));
break;
case "SetText":
this.SetText(edit.root, edit.text);
this.SetText(BigInt(edit.root), edit.text);
break;
case "SetAttribute":
this.SetAttribute(edit.root, edit.field, edit.value, edit.ns);
this.SetAttribute(BigInt(edit.root), edit.field, edit.value, edit.ns);
break;
case "RemoveAttribute":
this.RemoveAttribute(edit.root, edit.name);
this.RemoveAttribute(BigInt(edit.root), edit.name, edit.ns);
break;
case "PopRoot":
this.PopRoot();
break;
case "CreateTemplateRef":
this.CreateTemplateRef(BigInt(edit.id), edit.template_id);
break;
case "CreateTemplate":
this.CreateTemplate(BigInt(edit.id));
break;
case "FinishTemplate":
this.FinishTemplate(edit.len);
break;
case "EnterTemplateRef":
this.EnterTemplateRef(BigInt(edit.root));
break;
case "ExitTemplateRef":
this.ExitTemplateRef();
break;
case "CreateElementTemplate":
this.CreateElementTemplate(edit.tag, BigInt(edit.root), edit.locally_static, edit.fully_static);
break;
case "CreateElementNsTemplate":
this.CreateElementNsTemplate(edit.tag, BigInt(edit.root), edit.ns, edit.locally_static, edit.fully_static);
break;
case "CreateTextNodeTemplate":
this.CreateTextNodeTemplate(edit.text, BigInt(edit.root), edit.locally_static);
break;
case "CreatePlaceholderTemplate":
this.CreatePlaceholderTemplate(BigInt(edit.root));
break;
}
}
@ -346,6 +705,7 @@ function serialize_event(event) {
location,
repeat,
which,
code,
} = event;
return {
char_code: charCode,
@ -358,6 +718,7 @@ function serialize_event(event) {
location: location,
repeat: repeat,
which: which,
code,
};
}
case "focus":
@ -383,9 +744,11 @@ function serialize_event(event) {
case "submit": {
let target = event.target;
let value = target.value ?? target.textContent;
if (target.type === "checkbox") {
value = target.checked ? "true" : "false";
}
return {
value: value,
values: {},
@ -394,6 +757,7 @@ function serialize_event(event) {
case "click":
case "contextmenu":
case "doubleclick":
case "dblclick":
case "drag":
case "dragend":
case "dragenter":
@ -613,3 +977,176 @@ const bool_attrs = {
selected: true,
truespeed: true,
};
function is_element_node(node) {
return node.nodeType == 1;
}
function event_bubbles(event) {
switch (event) {
case "copy":
return true;
case "cut":
return true;
case "paste":
return true;
case "compositionend":
return true;
case "compositionstart":
return true;
case "compositionupdate":
return true;
case "keydown":
return true;
case "keypress":
return true;
case "keyup":
return true;
case "focus":
return false;
case "focusout":
return true;
case "focusin":
return true;
case "blur":
return false;
case "change":
return true;
case "input":
return true;
case "invalid":
return true;
case "reset":
return true;
case "submit":
return true;
case "click":
return true;
case "contextmenu":
return true;
case "doubleclick":
return true;
case "dblclick":
return true;
case "drag":
return true;
case "dragend":
return true;
case "dragenter":
return false;
case "dragexit":
return false;
case "dragleave":
return true;
case "dragover":
return true;
case "dragstart":
return true;
case "drop":
return true;
case "mousedown":
return true;
case "mouseenter":
return false;
case "mouseleave":
return false;
case "mousemove":
return true;
case "mouseout":
return true;
case "scroll":
return false;
case "mouseover":
return true;
case "mouseup":
return true;
case "pointerdown":
return true;
case "pointermove":
return true;
case "pointerup":
return true;
case "pointercancel":
return true;
case "gotpointercapture":
return true;
case "lostpointercapture":
return true;
case "pointerenter":
return false;
case "pointerleave":
return false;
case "pointerover":
return true;
case "pointerout":
return true;
case "select":
return true;
case "touchcancel":
return true;
case "touchend":
return true;
case "touchmove":
return true;
case "touchstart":
return true;
case "wheel":
return true;
case "abort":
return false;
case "canplay":
return false;
case "canplaythrough":
return false;
case "durationchange":
return false;
case "emptied":
return false;
case "encrypted":
return true;
case "ended":
return false;
case "error":
return false;
case "loadeddata":
return false;
case "loadedmetadata":
return false;
case "loadstart":
return false;
case "pause":
return false;
case "play":
return false;
case "playing":
return false;
case "progress":
return false;
case "ratechange":
return false;
case "seeked":
return false;
case "seeking":
return false;
case "stalled":
return false;
case "suspend":
return false;
case "timeupdate":
return false;
case "volumechange":
return false;
case "waiting":
return false;
case "animationstart":
return true;
case "animationend":
return true;
case "animationiteration":
return true;
case "transitionend":
return true;
case "toggle":
return true;
}
}

View file

@ -164,20 +164,20 @@ fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream {
let gen = quote! {
impl State for #type_name {
fn update<'a, T: dioxus_native_core::traversable::Traversable<Node = Self, Id = dioxus_core::ElementId>>(
dirty: &[(dioxus_core::ElementId, dioxus_native_core::node_ref::NodeMask)],
fn update<'a, T: dioxus_native_core::traversable::Traversable<Node = Self, Id = dioxus_core::GlobalNodeId>,T2: dioxus_native_core::traversable::Traversable<Node = dioxus_native_core::real_dom::NodeData, Id = dioxus_core::GlobalNodeId>>(
dirty: &[(dioxus_core::GlobalNodeId, dioxus_native_core::node_ref::NodeMask)],
state_tree: &'a mut T,
vdom: &'a dioxus_core::VirtualDom,
rdom: &'a T2,
ctx: &anymap::AnyMap,
) -> fxhash::FxHashSet<dioxus_core::ElementId>{
) -> fxhash::FxHashSet<dioxus_core::GlobalNodeId>{
#[derive(Eq, PartialEq)]
struct HeightOrdering {
height: u16,
id: dioxus_core::ElementId,
id: dioxus_core::GlobalNodeId,
}
impl HeightOrdering {
fn new(height: u16, id: dioxus_core::ElementId) -> Self {
fn new(height: u16, id: dioxus_core::GlobalNodeId) -> Self {
HeightOrdering {
height,
id,
@ -185,9 +185,29 @@ fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream {
}
}
// not the ordering after height is just for deduplication it can be any ordering as long as it is consistent
impl Ord for HeightOrdering {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.height.cmp(&other.height).then(self.id.0.cmp(&other.id.0))
self.height.cmp(&other.height).then(match (self.id, other.id){
(
dioxus_core::GlobalNodeId::TemplateId {
template_ref_id,
template_node_id,
},
dioxus_core::GlobalNodeId::TemplateId {
template_ref_id: o_template_ref_id,
template_node_id: o_template_node_id,
},
) => template_ref_id
.0
.cmp(&o_template_ref_id.0)
.then(template_node_id.0.cmp(&o_template_node_id.0)),
(dioxus_core::GlobalNodeId::TemplateId { .. }, dioxus_core::GlobalNodeId::VNodeId(_)) => std::cmp::Ordering::Less,
(dioxus_core::GlobalNodeId::VNodeId(_), dioxus_core::GlobalNodeId::TemplateId { .. }) => {
std::cmp::Ordering::Greater
}
(dioxus_core::GlobalNodeId::VNodeId(s_id), dioxus_core::GlobalNodeId::VNodeId(o_id)) => s_id.0.cmp(&o_id.0),
})
}
}
@ -218,7 +238,7 @@ fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream {
let mut dirty_elements = fxhash::FxHashSet::default();
// the states of any elements that are dirty
let mut states: fxhash::FxHashMap<dioxus_core::ElementId, MembersDirty> = fxhash::FxHashMap::default();
let mut states: fxhash::FxHashMap<dioxus_core::GlobalNodeId, MembersDirty> = fxhash::FxHashMap::default();
for (id, mask) in dirty {
let members_dirty = MembersDirty {
@ -244,7 +264,7 @@ fn impl_derive_macro(ast: &syn::DeriveInput) -> TokenStream {
<#child_types as dioxus_native_core::state::State>::update(
dirty,
&mut state_tree.map(|n| &n.#child_members, |n| &mut n.#child_members),
vdom,
rdom,
ctx,
)
);
@ -493,7 +513,7 @@ impl<'a> StateStruct<'a> {
let mut i = 0;
while i < resolution_order.len(){
let id = resolution_order[i].id;
let vnode = vdom.get_element(id).unwrap();
let node = rdom.get(id).unwrap();
let members_dirty = states.get_mut(&id).unwrap();
let (current_state, parent) = state_tree.get_node_parent_mut(id);
let current_state = current_state.unwrap();
@ -515,7 +535,7 @@ impl<'a> StateStruct<'a> {
let mut i = 0;
while i < resolution_order.len(){
let id = resolution_order[i].id;
let vnode = vdom.get_element(id).unwrap();
let node = rdom.get(id).unwrap();
let members_dirty = states.get_mut(&id).unwrap();
let (current_state, children) = state_tree.get_node_children_mut(id);
let current_state = current_state.unwrap();
@ -534,7 +554,7 @@ impl<'a> StateStruct<'a> {
let mut i = 0;
while i < resolution_order.len(){
let id = resolution_order[i];
let vnode = vdom.get_element(id).unwrap();
let node = rdom.get(id).unwrap();
let members_dirty = states.get_mut(&id).unwrap();
let current_state = state_tree.get_mut(id).unwrap();
if members_dirty.#member && #reduce_member {
@ -679,8 +699,7 @@ impl<'a> StateMember<'a> {
};
let ty = &self.mem.ty;
let node_view =
quote!(dioxus_native_core::node_ref::NodeView::new(vnode, #ty::NODE_MASK, vdom));
let node_view = quote!(dioxus_native_core::node_ref::NodeView::new(node, #ty::NODE_MASK));
let dep_idents = self.dep_mems.iter().map(|m| &m.0.ident);
match self.dep_kind {
DependencyKind::Node => {

View file

@ -109,7 +109,7 @@ macro_rules! test_state{
let mut dom: RealDom<$s> = RealDom::new();
let nodes_updated = dom.apply_mutations(vec![mutations]);
let _to_rerender = dom.update_state(&vdom, nodes_updated, AnyMap::new());
let _to_rerender = dom.update_state(nodes_updated, AnyMap::new());
dom.traverse_depth_first(|n| {
$(

View file

@ -1,10 +1,8 @@
use dioxus::core as dioxus_core;
use dioxus::core::{ElementId, VElement};
use dioxus::core::{self as dioxus_core, GlobalNodeId};
use dioxus::prelude::*;
use dioxus_native_core::real_dom::RealDom;
use dioxus_native_core::state::State;
use dioxus_native_core_macro::State;
use std::cell::Cell;
#[derive(State, Default, Clone)]
struct Empty {}
@ -18,44 +16,8 @@ fn remove_node() {
let vdom = VirtualDom::new(Base);
let mutations = vdom.create_vnodes(rsx! {
div{
div{}
}
});
let mut dom: RealDom<Empty> = RealDom::new();
let _to_update = dom.apply_mutations(vec![mutations]);
let child_div = VElement {
id: Cell::new(Some(ElementId(2))),
key: None,
tag: "div",
namespace: None,
parent: Cell::new(Some(ElementId(1))),
listeners: &[],
attributes: &[],
children: &[],
};
let child_div_el = VNode::Element(&child_div);
let root_div = VElement {
id: Cell::new(Some(ElementId(1))),
key: None,
tag: "div",
namespace: None,
parent: Cell::new(Some(ElementId(0))),
listeners: &[],
attributes: &[],
children: &[child_div_el],
};
assert_eq!(dom.size(), 2);
assert!(&dom.contains_node(&VNode::Element(&root_div)));
assert_eq!(dom[ElementId(1)].height, 1);
assert_eq!(dom[ElementId(2)].height, 2);
let vdom = VirtualDom::new(Base);
let mutations = vdom.diff_lazynodes(
let (create, edit) = vdom.diff_lazynodes(
rsx! {
div{
div{}
@ -65,22 +27,43 @@ fn remove_node() {
div{}
},
);
dom.apply_mutations(vec![mutations.1]);
let new_root_div = VElement {
id: Cell::new(Some(ElementId(1))),
key: None,
tag: "div",
namespace: None,
parent: Cell::new(Some(ElementId(0))),
listeners: &[],
attributes: &[],
children: &[],
};
println!("create: {:#?}", create);
println!("edit: {:#?}", edit);
let _to_update = dom.apply_mutations(vec![create]);
assert_eq!(
dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(1),
template_node_id: dioxus::prelude::TemplateNodeId(0),
}]
.node_data
.height,
1
);
assert_eq!(
dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(1),
template_node_id: dioxus::prelude::TemplateNodeId(1),
}]
.node_data
.height,
2
);
dom.apply_mutations(vec![edit]);
assert_eq!(dom.size(), 1);
assert!(&dom.contains_node(&VNode::Element(&new_root_div)));
assert_eq!(dom[ElementId(1)].height, 1);
assert_eq!(
dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(2),
template_node_id: dioxus::prelude::TemplateNodeId(0),
}]
.node_data
.height,
1
);
}
#[test]
@ -92,31 +75,7 @@ fn add_node() {
let vdom = VirtualDom::new(Base);
let mutations = vdom.create_vnodes(rsx! {
div{}
});
let mut dom: RealDom<Empty> = RealDom::new();
let _to_update = dom.apply_mutations(vec![mutations]);
let root_div = VElement {
id: Cell::new(Some(ElementId(1))),
key: None,
tag: "div",
namespace: None,
parent: Cell::new(Some(ElementId(0))),
listeners: &[],
attributes: &[],
children: &[],
};
assert_eq!(dom.size(), 1);
assert!(&dom.contains_node(&VNode::Element(&root_div)));
assert_eq!(dom[ElementId(1)].height, 1);
let vdom = VirtualDom::new(Base);
let mutations = vdom.diff_lazynodes(
let (create, update) = vdom.diff_lazynodes(
rsx! {
div{}
},
@ -126,32 +85,41 @@ fn add_node() {
}
},
);
dom.apply_mutations(vec![mutations.1]);
let child_div = VElement {
id: Cell::new(Some(ElementId(2))),
key: None,
tag: "p",
namespace: None,
parent: Cell::new(Some(ElementId(1))),
listeners: &[],
attributes: &[],
children: &[],
};
let child_div_el = VNode::Element(&child_div);
let new_root_div = VElement {
id: Cell::new(Some(ElementId(1))),
key: None,
tag: "div",
namespace: None,
parent: Cell::new(Some(ElementId(0))),
listeners: &[],
attributes: &[],
children: &[child_div_el],
};
let mut dom: RealDom<Empty> = RealDom::new();
assert_eq!(dom.size(), 2);
assert!(&dom.contains_node(&VNode::Element(&new_root_div)));
assert_eq!(dom[ElementId(1)].height, 1);
assert_eq!(dom[ElementId(2)].height, 2);
let _to_update = dom.apply_mutations(vec![create]);
assert_eq!(dom.size(), 1);
assert_eq!(
dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(1),
template_node_id: dioxus::prelude::TemplateNodeId(0),
}]
.node_data
.height,
1
);
dom.apply_mutations(vec![update]);
assert_eq!(dom.size(), 1);
assert_eq!(
dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(2),
template_node_id: dioxus::prelude::TemplateNodeId(0),
}]
.node_data
.height,
1
);
assert_eq!(
dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(2),
template_node_id: dioxus::prelude::TemplateNodeId(1),
}]
.node_data
.height,
2
);
}

View file

@ -1,7 +1,4 @@
use std::cell::Cell;
use dioxus::core as dioxus_core;
use dioxus::core::{ElementId, VElement, VText};
use dioxus::core::{self as dioxus_core, GlobalNodeId};
use dioxus::prelude::*;
use dioxus_native_core::real_dom::RealDom;
use dioxus_native_core::state::State;
@ -12,8 +9,6 @@ struct Empty {}
#[test]
fn initial_build_simple() {
use std::cell::Cell;
#[allow(non_snake_case)]
fn Base(cx: Scope) -> Element {
render!(div {})
@ -28,19 +23,17 @@ fn initial_build_simple() {
let mut dom: RealDom<Empty> = RealDom::new();
let _to_update = dom.apply_mutations(vec![mutations]);
let root_div = VElement {
id: Cell::new(Some(ElementId(1))),
key: None,
tag: "div",
namespace: None,
parent: Cell::new(Some(ElementId(0))),
listeners: &[],
attributes: &[],
children: &[],
};
assert_eq!(dom.size(), 1);
assert!(&dom.contains_node(&VNode::Element(&root_div)));
assert_eq!(dom[ElementId(1)].height, 1);
assert_eq!(
dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(1),
template_node_id: dioxus::prelude::TemplateNodeId(0),
}]
.node_data
.height,
1
);
}
#[test]
@ -67,62 +60,59 @@ fn initial_build_with_children() {
let mut dom: RealDom<Empty> = RealDom::new();
let _to_update = dom.apply_mutations(vec![mutations]);
let first_text = VText {
id: Cell::new(Some(ElementId(3))),
text: "hello",
is_static: true,
};
let first_text_node = VNode::Text(&first_text);
let child_text = VText {
id: Cell::new(Some(ElementId(5))),
text: "world",
is_static: true,
};
let child_text_node = VNode::Text(&child_text);
let child_p_el = VElement {
id: Cell::new(Some(ElementId(4))),
key: None,
tag: "p",
namespace: None,
parent: Cell::new(Some(ElementId(2))),
listeners: &[],
attributes: &[],
children: &[child_text_node],
};
let child_p_node = VNode::Element(&child_p_el);
let second_text = VText {
id: Cell::new(Some(ElementId(6))),
text: "hello world",
is_static: true,
};
let second_text_node = VNode::Text(&second_text);
let child_div_el = VElement {
id: Cell::new(Some(ElementId(2))),
key: None,
tag: "div",
namespace: None,
parent: Cell::new(Some(ElementId(1))),
listeners: &[],
attributes: &[],
children: &[first_text_node, child_p_node, second_text_node],
};
let child_div_node = VNode::Element(&child_div_el);
let root_div = VElement {
id: Cell::new(Some(ElementId(1))),
key: None,
tag: "div",
namespace: None,
parent: Cell::new(Some(ElementId(0))),
listeners: &[],
attributes: &[],
children: &[child_div_node],
};
assert_eq!(dom.size(), 6);
assert!(&dom.contains_node(&VNode::Element(&root_div)));
assert_eq!(dom[ElementId(1)].height, 1);
assert_eq!(dom[ElementId(2)].height, 2);
assert_eq!(dom[ElementId(3)].height, 3);
assert_eq!(dom[ElementId(4)].height, 3);
assert_eq!(dom[ElementId(5)].height, 4);
assert_eq!(dom[ElementId(6)].height, 3);
assert_eq!(dom.size(), 1);
assert_eq!(
dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(1),
template_node_id: dioxus::prelude::TemplateNodeId(0),
}]
.node_data
.height,
1
);
assert_eq!(
dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(1),
template_node_id: dioxus::prelude::TemplateNodeId(1),
}]
.node_data
.height,
2
);
assert_eq!(
dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(1),
template_node_id: dioxus::prelude::TemplateNodeId(2),
}]
.node_data
.height,
3
);
assert_eq!(
dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(1),
template_node_id: dioxus::prelude::TemplateNodeId(3),
}]
.node_data
.height,
3
);
assert_eq!(
dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(1),
template_node_id: dioxus::prelude::TemplateNodeId(4),
}]
.node_data
.height,
4
);
assert_eq!(
dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(1),
template_node_id: dioxus::prelude::TemplateNodeId(5),
}]
.node_data
.height,
3
);
}

View file

@ -1,4 +1,5 @@
use dioxus::core as dioxus_core;
use dioxus::core_macro::rsx_without_templates;
use dioxus::prelude::*;
use dioxus_native_core::{
real_dom::{NodeType, RealDom},
@ -37,68 +38,68 @@ fn traverse() {
let mut iter = PersistantElementIter::new();
let div_tag = "div".to_string();
assert!(matches!(
&rdom[iter.next(&rdom).id()].node_type,
&rdom[iter.next(&rdom).id()].node_data.node_type,
NodeType::Element { tag: div_tag, .. }
));
assert!(matches!(
&rdom[iter.next(&rdom).id()].node_type,
&rdom[iter.next(&rdom).id()].node_data.node_type,
NodeType::Element { tag: div_tag, .. }
));
let text1 = "hello".to_string();
assert!(matches!(
&rdom[iter.next(&rdom).id()].node_type,
&rdom[iter.next(&rdom).id()].node_data.node_type,
NodeType::Text { text: text1, .. }
));
let p_tag = "p".to_string();
assert!(matches!(
&rdom[iter.next(&rdom).id()].node_type,
&rdom[iter.next(&rdom).id()].node_data.node_type,
NodeType::Element { tag: p_tag, .. }
));
let text2 = "world".to_string();
assert!(matches!(
&rdom[iter.next(&rdom).id()].node_type,
&rdom[iter.next(&rdom).id()].node_data.node_type,
NodeType::Text { text: text2, .. }
));
let text3 = "hello world".to_string();
assert!(matches!(
&rdom[iter.next(&rdom).id()].node_type,
&rdom[iter.next(&rdom).id()].node_data.node_type,
NodeType::Text { text: text3, .. }
));
assert!(matches!(
&rdom[iter.next(&rdom).id()].node_type,
&rdom[iter.next(&rdom).id()].node_data.node_type,
NodeType::Element { tag: div_tag, .. }
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_type,
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Text { text: text3, .. }
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_type,
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Text { text: text2, .. }
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_type,
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Element { tag: p_tag, .. }
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_type,
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Text { text: text1, .. }
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_type,
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Element { tag: div_tag, .. }
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_type,
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Element { tag: div_tag, .. }
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_type,
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Element { tag: div_tag, .. }
));
assert!(matches!(
&rdom[iter.prev(&rdom).id()].node_type,
&rdom[iter.prev(&rdom).id()].node_data.node_type,
NodeType::Text { text: text3, .. }
));
}
@ -112,7 +113,7 @@ fn persist_removes() {
}
let vdom = VirtualDom::new(Base);
let (build, update) = vdom.diff_lazynodes(
rsx! {
rsx_without_templates! {
div{
p{
key: "1",
@ -128,7 +129,7 @@ fn persist_removes() {
}
}
},
rsx! {
rsx_without_templates! {
div{
p{
key: "1",
@ -177,16 +178,19 @@ fn persist_removes() {
let p_tag = "p".to_string();
let idx = iter1.next(&rdom).id();
assert!(matches!(
&rdom[idx].node_type,
&rdom[idx].node_data.node_type,
NodeType::Element { tag: p_tag, .. }
));
let text = "hello world".to_string();
let idx = iter1.next(&rdom).id();
assert!(matches!(&rdom[idx].node_type, NodeType::Text { text, .. }));
assert!(matches!(
&rdom[idx].node_data.node_type,
NodeType::Text { text, .. }
));
let div_tag = "div".to_string();
let idx = iter2.next(&rdom).id();
assert!(matches!(
&rdom[idx].node_type,
&rdom[idx].node_data.node_type,
NodeType::Element { tag: div_tag, .. }
));
}
@ -200,7 +204,7 @@ fn persist_instertions_before() {
}
let vdom = VirtualDom::new(Base);
let (build, update) = vdom.diff_lazynodes(
rsx! {
rsx_without_templates! {
div{
p{
key: "1",
@ -212,7 +216,7 @@ fn persist_instertions_before() {
}
}
},
rsx! {
rsx_without_templates! {
div{
p{
key: "1",
@ -252,7 +256,7 @@ fn persist_instertions_before() {
let p_tag = "div".to_string();
let idx = iter.next(&rdom).id();
assert!(matches!(
&rdom[idx].node_type,
&rdom[idx].node_data.node_type,
NodeType::Element { tag: p_tag, .. }
));
}
@ -266,7 +270,7 @@ fn persist_instertions_after() {
}
let vdom = VirtualDom::new(Base);
let (build, update) = vdom.diff_lazynodes(
rsx! {
rsx_without_templates! {
div{
p{
key: "1",
@ -278,7 +282,7 @@ fn persist_instertions_after() {
}
}
},
rsx! {
rsx_without_templates! {
div{
p{
key: "1",
@ -318,10 +322,13 @@ fn persist_instertions_after() {
let p_tag = "p".to_string();
let idx = iter.next(&rdom).id();
assert!(matches!(
&rdom[idx].node_type,
&rdom[idx].node_data.node_type,
NodeType::Element { tag: p_tag, .. }
));
let text = "hello world".to_string();
let idx = iter.next(&rdom).id();
assert!(matches!(&rdom[idx].node_type, NodeType::Text { text, .. }));
assert!(matches!(
&rdom[idx].node_data.node_type,
NodeType::Text { text, .. }
));
}

View file

@ -1,7 +1,8 @@
use anymap::AnyMap;
use dioxus::core as dioxus_core;
use dioxus::core::ElementId;
use dioxus::core::{self as dioxus_core, GlobalNodeId};
use dioxus::core::{AttributeValue, DomEdit, Mutations};
use dioxus::core_macro::rsx_without_templates;
use dioxus::prelude::*;
use dioxus_native_core::node_ref::*;
use dioxus_native_core::real_dom::*;
@ -50,14 +51,13 @@ impl ChildDepState for ChildDepCallCounter {
const NODE_MASK: NodeMask = NodeMask::ALL;
fn reduce<'a>(
&mut self,
node: NodeView,
_: NodeView,
_: impl Iterator<Item = &'a Self::DepState>,
_: &Self::Ctx,
) -> bool
where
Self::DepState: 'a,
{
println!("{self:?} {:?}: {} {:?}", node.tag(), node.id(), node.text());
self.0 += 1;
true
}
@ -142,8 +142,16 @@ impl NodeDepState<()> for NodeStateTester {
*self = NodeStateTester(
node.tag().map(|s| s.to_string()),
node.attributes()
.map(|a| (a.name.to_string(), a.value.to_string()))
.collect(),
.map(|iter| {
iter.map(|a| {
(
a.attribute.name.to_string(),
format!("{}", a.value.as_text().unwrap()),
)
})
.collect()
})
.unwrap_or_default(),
);
true
}
@ -185,9 +193,12 @@ fn state_initial() {
let nodes_updated = dom.apply_mutations(vec![mutations]);
let mut ctx = AnyMap::new();
ctx.insert(42u32);
let _to_rerender = dom.update_state(&vdom, nodes_updated, ctx);
let _to_rerender = dom.update_state(nodes_updated, ctx);
let root_div = &dom[ElementId(1)];
let root_div = &dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(1),
template_node_id: dioxus::prelude::TemplateNodeId(0),
}];
assert_eq!(root_div.state.bubbled.0, Some("div".to_string()));
assert_eq!(
root_div.state.bubbled.1,
@ -199,12 +210,18 @@ fn state_initial() {
assert_eq!(root_div.state.pushed.0, Some("div".to_string()));
assert_eq!(
root_div.state.pushed.1,
Some(Box::new(PushedDownStateTester(None, None)))
Some(Box::new(PushedDownStateTester(
Some("Root".to_string()),
None
)))
);
assert_eq!(root_div.state.node.0, Some("div".to_string()));
assert_eq!(root_div.state.node.1, vec![]);
let child_p = &dom[ElementId(2)];
let child_p = &dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(1),
template_node_id: dioxus::prelude::TemplateNodeId(1),
}];
assert_eq!(child_p.state.bubbled.0, Some("p".to_string()));
assert_eq!(child_p.state.bubbled.1, Vec::new());
assert_eq!(child_p.state.pushed.0, Some("p".to_string()));
@ -212,7 +229,10 @@ fn state_initial() {
child_p.state.pushed.1,
Some(Box::new(PushedDownStateTester(
Some("div".to_string()),
Some(Box::new(PushedDownStateTester(None, None)))
Some(Box::new(PushedDownStateTester(
Some("Root".to_string()),
None
)))
)))
);
assert_eq!(child_p.state.node.0, Some("p".to_string()));
@ -221,7 +241,10 @@ fn state_initial() {
vec![("color".to_string(), "red".to_string())]
);
let child_h1 = &dom[ElementId(3)];
let child_h1 = &dom[GlobalNodeId::TemplateId {
template_ref_id: dioxus_core::ElementId(1),
template_node_id: dioxus::prelude::TemplateNodeId(2),
}];
assert_eq!(child_h1.state.bubbled.0, Some("h1".to_string()));
assert_eq!(child_h1.state.bubbled.1, Vec::new());
assert_eq!(child_h1.state.pushed.0, Some("h1".to_string()));
@ -229,7 +252,10 @@ fn state_initial() {
child_h1.state.pushed.1,
Some(Box::new(PushedDownStateTester(
Some("div".to_string()),
Some(Box::new(PushedDownStateTester(None, None)))
Some(Box::new(PushedDownStateTester(
Some("Root".to_string()),
None
)))
)))
);
assert_eq!(child_h1.state.node.0, Some("h1".to_string()));
@ -261,7 +287,7 @@ fn state_reduce_parent_called_minimally_on_update() {
let vdom = VirtualDom::new(Base);
let mutations = vdom.create_vnodes(rsx! {
let mutations = vdom.create_vnodes(rsx_without_templates! {
div {
width: "100%",
div{
@ -284,7 +310,7 @@ fn state_reduce_parent_called_minimally_on_update() {
let mut dom: RealDom<CallCounterState> = RealDom::new();
let nodes_updated = dom.apply_mutations(vec![mutations]);
let _to_rerender = dom.update_state(&vdom, nodes_updated, AnyMap::new());
let _to_rerender = dom.update_state(nodes_updated, AnyMap::new());
let nodes_updated = dom.apply_mutations(vec![Mutations {
edits: vec![DomEdit::SetAttribute {
root: 1,
@ -295,7 +321,7 @@ fn state_reduce_parent_called_minimally_on_update() {
dirty_scopes: fxhash::FxHashSet::default(),
refs: Vec::new(),
}]);
let _to_rerender = dom.update_state(&vdom, nodes_updated, AnyMap::new());
let _to_rerender = dom.update_state(nodes_updated, AnyMap::new());
dom.traverse_depth_first(|n| {
assert_eq!(n.state.part2.parent_counter.0, 2);
@ -307,7 +333,7 @@ fn state_reduce_parent_called_minimally_on_update() {
fn state_reduce_child_called_minimally_on_update() {
#[allow(non_snake_case)]
fn Base(cx: Scope) -> Element {
render!(div {
cx.render(rsx_without_templates!(div {
div{
div{
p{
@ -324,12 +350,12 @@ fn state_reduce_child_called_minimally_on_update() {
"world"
}
}
})
}))
}
let vdom = VirtualDom::new(Base);
let mutations = vdom.create_vnodes(rsx! {
let mutations = vdom.create_vnodes(rsx_without_templates! {
div {
div{
div{
@ -353,7 +379,7 @@ fn state_reduce_child_called_minimally_on_update() {
let mut dom: RealDom<CallCounterState> = RealDom::new();
let nodes_updated = dom.apply_mutations(vec![mutations]);
let _to_rerender = dom.update_state(&vdom, nodes_updated, AnyMap::new());
let _to_rerender = dom.update_state(nodes_updated, AnyMap::new());
let nodes_updated = dom.apply_mutations(vec![Mutations {
edits: vec![DomEdit::SetAttribute {
root: 4,
@ -364,15 +390,33 @@ fn state_reduce_child_called_minimally_on_update() {
dirty_scopes: fxhash::FxHashSet::default(),
refs: Vec::new(),
}]);
let _to_rerender = dom.update_state(&vdom, nodes_updated, AnyMap::new());
let _to_rerender = dom.update_state(nodes_updated, AnyMap::new());
dom.traverse_depth_first(|n| {
println!("{:?}", n);
assert_eq!(
n.state.part1.child_counter.0,
if n.id.0 > 4 { 1 } else { 2 }
if let GlobalNodeId::VNodeId(ElementId(id)) = n.node_data.id {
if id > 4 {
1
} else {
2
}
} else {
panic!()
}
);
assert_eq!(
n.state.child_counter.0,
if let GlobalNodeId::VNodeId(ElementId(id)) = n.node_data.id {
if id > 4 {
1
} else {
2
}
} else {
panic!()
}
);
assert_eq!(n.state.child_counter.0, if n.id.0 > 4 { 1 } else { 2 });
});
}
@ -457,7 +501,7 @@ fn dependancies_order_independant() {
let mut dom: RealDom<UnorderedDependanciesState> = RealDom::new();
let nodes_updated = dom.apply_mutations(vec![mutations]);
let _to_rerender = dom.update_state(&vdom, nodes_updated, AnyMap::new());
let _to_rerender = dom.update_state(nodes_updated, AnyMap::new());
let c = CDepCallCounter(1);
let b = BDepCallCounter(1, c.clone());

View file

@ -1 +1 @@
# Dioxus Native-Core: An ECS approach to GUI state and trees
# Dioxus Native-Core: An lazy approach to GUI state and trees

View file

@ -2,6 +2,7 @@ pub mod layout_attributes;
pub mod node_ref;
pub mod real_dom;
pub mod state;
pub mod template;
#[doc(hidden)]
pub mod traversable;
pub mod utils;

View file

@ -1,37 +1,39 @@
use dioxus_core::*;
use crate::state::union_ordered_iter;
use crate::{
real_dom::{NodeData, NodeType, OwnedAttributeView},
state::union_ordered_iter,
};
/// A view into a [VNode] with limited access.
#[derive(Debug)]
pub struct NodeView<'a> {
inner: &'a VNode<'a>,
inner: &'a NodeData,
mask: NodeMask,
}
impl<'a> NodeView<'a> {
/// Create a new NodeView from a VNode, and mask.
pub fn new(mut vnode: &'a VNode<'a>, view: NodeMask, vdom: &'a VirtualDom) -> Self {
if let VNode::Component(sc) = vnode {
let scope = vdom.get_scope(sc.scope.get().unwrap()).unwrap();
vnode = scope.root_node();
}
pub fn new(node: &'a NodeData, view: NodeMask) -> Self {
Self {
inner: vnode,
inner: node,
mask: view,
}
}
/// Get the id of the node
pub fn id(&self) -> ElementId {
self.inner.mounted_id()
pub fn id(&self) -> GlobalNodeId {
self.inner.id
}
/// Get the tag of the node if the tag is enabled in the mask
pub fn tag(&self) -> Option<&'a str> {
self.mask
.tag
.then(|| self.try_element().map(|el| el.tag))
.then(|| match &self.inner.node_type {
NodeType::Element { tag, .. } => Some(&**tag),
_ => None,
})
.flatten()
}
@ -39,47 +41,47 @@ impl<'a> NodeView<'a> {
pub fn namespace(&self) -> Option<&'a str> {
self.mask
.namespace
.then(|| self.try_element().and_then(|el| el.namespace))
.then(|| match &self.inner.node_type {
NodeType::Element { namespace, .. } => namespace.map(|s| &*s),
_ => None,
})
.flatten()
}
/// Get any attributes that are enabled in the mask
pub fn attributes(&self) -> impl Iterator<Item = &Attribute<'a>> {
self.try_element()
.map(|el| el.attributes)
.unwrap_or_default()
.iter()
.filter(|a| self.mask.attritutes.contains_attribute(a.name))
pub fn attributes<'b>(&'b self) -> Option<impl Iterator<Item = OwnedAttributeView<'a>> + 'b> {
match &self.inner.node_type {
NodeType::Element { attributes, .. } => Some(
attributes
.iter()
.filter(move |(attr, _)| self.mask.attritutes.contains_attribute(&attr.name))
.map(|(attr, val)| OwnedAttributeView {
attribute: attr,
value: val,
}),
),
_ => None,
}
}
/// Get the text if it is enabled in the mask
pub fn text(&self) -> Option<&str> {
self.mask
.text
.then(|| self.try_text().map(|txt| txt.text))
.then(|| match &self.inner.node_type {
NodeType::Text { text } => Some(&**text),
_ => None,
})
.flatten()
}
/// Get the listeners if it is enabled in the mask
pub fn listeners(&self) -> &'a [Listener<'a>] {
self.try_element()
.map(|el| el.listeners)
.unwrap_or_default()
}
/// Try to get the underlying element.
fn try_element(&self) -> Option<&'a VElement<'a>> {
if let VNode::Element(el) = &self.inner {
Some(el)
} else {
None
}
}
/// Try to get the underlying text node.
fn try_text(&self) -> Option<&'a VText<'a>> {
if let VNode::Text(txt) = &self.inner {
Some(txt)
pub fn listeners(&self) -> Option<impl Iterator<Item = &'a str> + '_> {
if self.mask.listeners {
match &self.inner.node_type {
NodeType::Element { listeners, .. } => Some(listeners.iter().map(|l| &**l)),
_ => None,
}
} else {
None
}
@ -100,7 +102,7 @@ impl AttributeMask {
/// A empty attribute mask
pub const NONE: Self = Self::Static(&[]);
fn contains_attribute(&self, attr: &'static str) -> bool {
fn contains_attribute(&self, attr: &str) -> bool {
match self {
AttributeMask::All => true,
AttributeMask::Dynamic(l) => l.binary_search(&attr).is_ok(),
@ -215,7 +217,8 @@ impl NodeMask {
/// A node mask with every part visible.
pub const ALL: Self = Self::new_with_attrs(AttributeMask::All)
.with_text()
.with_element();
.with_element()
.with_listeners();
/// Check if two masks overlap
pub fn overlaps(&self, other: &Self) -> bool {

File diff suppressed because it is too large Load diff

View file

@ -1,10 +1,11 @@
use std::{cmp::Ordering, fmt::Debug};
use anymap::AnyMap;
use dioxus_core::ElementId;
use dioxus_core::GlobalNodeId;
use fxhash::FxHashSet;
use crate::node_ref::{NodeMask, NodeView};
use crate::real_dom::NodeData;
use crate::traversable::Traversable;
/// Join two sorted iterators
@ -206,12 +207,16 @@ pub trait NodeDepState<DepState = ()> {
/// Do not implement this trait. It is only meant to be derived and used through [crate::real_dom::RealDom].
pub trait State: Default + Clone {
#[doc(hidden)]
fn update<'a, T: Traversable<Node = Self, Id = ElementId>>(
dirty: &[(ElementId, NodeMask)],
fn update<
'a,
T: Traversable<Node = Self, Id = GlobalNodeId>,
T2: Traversable<Node = NodeData, Id = GlobalNodeId>,
>(
dirty: &[(GlobalNodeId, NodeMask)],
state_tree: &'a mut T,
vdom: &'a dioxus_core::VirtualDom,
rdom: &'a T2,
ctx: &AnyMap,
) -> FxHashSet<ElementId>;
) -> FxHashSet<GlobalNodeId>;
}
impl ChildDepState for () {

View file

@ -0,0 +1,44 @@
use dioxus_core::{GlobalNodeId, TemplateNodeId};
use crate::{real_dom::Node, state::State};
#[derive(Debug, Default)]
pub struct NativeTemplate<S: State> {
pub(crate) nodes: Vec<Option<Box<Node<S>>>>,
pub(crate) roots: Vec<usize>,
}
impl<S: State> NativeTemplate<S> {
pub fn insert(&mut self, node: Node<S>) {
let id = node.node_data.id;
match id {
GlobalNodeId::TemplateId {
template_node_id: TemplateNodeId(id),
..
} => {
self.nodes.resize(id + 1, None);
self.nodes[id] = Some(Box::new(node));
}
GlobalNodeId::VNodeId(_) => panic!("Cannot insert a VNode into a template"),
}
}
}
#[derive(Debug)]
pub(crate) enum TemplateRefOrNode<S: State> {
Ref {
nodes: Vec<Option<Box<Node<S>>>>,
roots: Vec<GlobalNodeId>,
parent: Option<GlobalNodeId>,
},
Node(Node<S>),
}
impl<S: State> TemplateRefOrNode<S> {
pub fn parent(&self) -> Option<GlobalNodeId> {
match self {
TemplateRefOrNode::Ref { parent, .. } => *parent,
TemplateRefOrNode::Node(node) => node.node_data.parent,
}
}
}

View file

@ -2,16 +2,16 @@ use crate::{
real_dom::{NodeType, RealDom},
state::State,
};
use dioxus_core::{DomEdit, ElementId, Mutations};
use dioxus_core::{DomEdit, ElementId, GlobalNodeId, Mutations};
pub enum ElementProduced {
/// The iterator produced an element by progressing to the next node in a depth first order.
Progressed(ElementId),
Progressed(GlobalNodeId),
/// The iterator reached the end of the tree and looped back to the root
Looped(ElementId),
Looped(GlobalNodeId),
}
impl ElementProduced {
pub fn id(&self) -> ElementId {
pub fn id(&self) -> GlobalNodeId {
match self {
ElementProduced::Progressed(id) => *id,
ElementProduced::Looped(id) => *id,
@ -50,13 +50,16 @@ impl NodePosition {
/// The iterator loops around when it reaches the end or the beginning.
pub struct PersistantElementIter {
// stack of elements and fragments
stack: smallvec::SmallVec<[(ElementId, NodePosition); 5]>,
stack: smallvec::SmallVec<[(GlobalNodeId, NodePosition); 5]>,
}
impl Default for PersistantElementIter {
fn default() -> Self {
PersistantElementIter {
stack: smallvec::smallvec![(ElementId(0), NodePosition::AtNode)],
stack: smallvec::smallvec![(
GlobalNodeId::VNodeId(dioxus_core::ElementId(0)),
NodePosition::AtNode
)],
}
}
}
@ -74,8 +77,10 @@ impl PersistantElementIter {
.edits
.iter()
.filter_map(|e| {
// nodes within templates will never be removed
if let DomEdit::Remove { root } = e {
Some(*root)
let id = rdom.decode_id(*root);
Some(id)
} else {
None
}
@ -85,7 +90,7 @@ impl PersistantElementIter {
if let Some(r) = self
.stack
.iter()
.position(|(el_id, _)| ids_removed.iter().any(|id| el_id.as_u64() == *id))
.position(|(el_id, _)| ids_removed.iter().any(|id| el_id == id))
{
self.stack.truncate(r);
changed = true;
@ -93,33 +98,24 @@ impl PersistantElementIter {
// if a child is removed or inserted before or at the current element, update the child index
for (el_id, child_idx) in self.stack.iter_mut() {
if let NodePosition::InChild(child_idx) = child_idx {
if let NodeType::Element { children, .. } = &rdom[*el_id].node_type {
if let NodeType::Element { children, .. } = &rdom[*el_id].node_data.node_type {
for m in &mutations.edits {
match m {
DomEdit::Remove { root } => {
if children
.iter()
.take(*child_idx + 1)
.any(|c| c.as_u64() == *root)
{
let id = rdom.decode_id(*root);
if children.iter().take(*child_idx + 1).any(|c| *c == id) {
*child_idx -= 1;
}
}
DomEdit::InsertBefore { root, n } => {
if children
.iter()
.take(*child_idx + 1)
.any(|c| c.as_u64() == *root)
{
let id = rdom.decode_id(*root);
if children.iter().take(*child_idx + 1).any(|c| *c == id) {
*child_idx += *n as usize;
}
}
DomEdit::InsertAfter { root, n } => {
if children
.iter()
.take(*child_idx)
.any(|c| c.as_u64() == *root)
{
let id = rdom.decode_id(*root);
if children.iter().take(*child_idx).any(|c| *c == id) {
*child_idx += *n as usize;
}
}
@ -135,14 +131,14 @@ impl PersistantElementIter {
/// get the next element
pub fn next<S: State>(&mut self, rdom: &RealDom<S>) -> ElementProduced {
if self.stack.is_empty() {
let id = ElementId(0);
let id = GlobalNodeId::VNodeId(ElementId(0));
let new = (id, NodePosition::AtNode);
self.stack.push(new);
ElementProduced::Looped(id)
} else {
let (last, o_child_idx) = self.stack.last_mut().unwrap();
let node = &rdom[*last];
match &node.node_type {
match &node.node_data.node_type {
NodeType::Element { children, .. } => {
*o_child_idx = o_child_idx.map(|i| i + 1);
// if we have children, go to the next child
@ -152,7 +148,7 @@ impl PersistantElementIter {
self.next(rdom)
} else {
let id = children[child_idx];
if let NodeType::Element { .. } = &rdom[id].node_type {
if let NodeType::Element { .. } = &rdom[id].node_data.node_type {
self.stack.push((id, NodePosition::AtNode));
}
ElementProduced::Progressed(id)
@ -171,11 +167,11 @@ impl PersistantElementIter {
pub fn prev<S: State>(&mut self, rdom: &RealDom<S>) -> ElementProduced {
// recursively add the last child element to the stack
fn push_back<S: State>(
stack: &mut smallvec::SmallVec<[(ElementId, NodePosition); 5]>,
new_node: ElementId,
stack: &mut smallvec::SmallVec<[(GlobalNodeId, NodePosition); 5]>,
new_node: GlobalNodeId,
rdom: &RealDom<S>,
) -> ElementId {
match &rdom[new_node].node_type {
) -> GlobalNodeId {
match &rdom[new_node].node_data.node_type {
NodeType::Element { children, .. } => {
if children.is_empty() {
new_node
@ -188,12 +184,12 @@ impl PersistantElementIter {
}
}
if self.stack.is_empty() {
let new_node = ElementId(0);
let new_node = GlobalNodeId::VNodeId(ElementId(0));
ElementProduced::Looped(push_back(&mut self.stack, new_node, rdom))
} else {
let (last, o_child_idx) = self.stack.last_mut().unwrap();
let node = &rdom[*last];
match &node.node_type {
match &node.node_data.node_type {
NodeType::Element { children, .. } => {
// if we have children, go to the next child
if let NodePosition::InChild(0) = o_child_idx {
@ -227,7 +223,7 @@ impl PersistantElementIter {
}
}
fn pop(&mut self) -> ElementId {
fn pop(&mut self) -> GlobalNodeId {
self.stack.pop().unwrap().0
}
}

View file

@ -7,6 +7,11 @@ license = "MIT/Apache-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
proc-macro2 = { version = "1.0" }
proc-macro2 = { version = "1.0", features = ["span-locations"] }
syn = { version = "1.0", features = ["full", "extra-traits"] }
quote = { version = "1.0" }
dioxus-core = { path = "../core", features = ["serialize", "hot-reload"] }
serde = { version = "1.0", features = ["derive"] }
[features]
hot-reload = []

View file

@ -1,32 +1,32 @@
use crate::elements::*;
// map the rsx name of the attribute to the html name of the attribute and the namespace that contains it
// map the rsx name of the attribute to the html name of the attribute, the namespace that contains it, and if the attribute is volitile
pub fn attrbute_to_static_str(
attr: &str,
element: &'static str,
namespace: Option<&'static str>,
) -> Option<(&'static str, Option<&'static str>)> {
) -> Option<(&'static str, Option<&'static str>, bool)> {
if namespace == Some("http://www.w3.org/2000/svg") {
svg::MAPPED_ATTRIBUTES
.iter()
.find(|(a, _)| *a == attr)
.map(|(_, b)| (*b, None))
.map(|(_, b)| (*b, None, false))
} else {
NO_NAMESPACE_ATTRIBUTES
.iter()
.find(|&a| *a == attr)
.map(|a| (*a, None))
.map(|a| (*a, None, false))
.or_else(|| {
STYLE_ATTRIBUTES
.iter()
.find(|(a, _)| *a == attr)
.map(|(_, b)| (*b, Some("style")))
.map(|(_, b)| (*b, Some("style"), false))
})
.or_else(|| {
MAPPED_ATTRIBUTES
.iter()
.find(|(a, _)| *a == attr)
.map(|(_, b)| (*b, None))
.map(|(_, b)| (*b, None, false))
})
}
.or_else(|| {
@ -37,8 +37,8 @@ pub fn attrbute_to_static_str(
.then(|| {
attrs
.iter()
.find(|(a, _)| *a == attr)
.map(|(_, b)| (*b, None))
.find(|(a, _, _)| *a == attr)
.map(|(_, b, volitile)| (*b, None, *volitile))
})
.flatten()
})
@ -46,14 +46,24 @@ pub fn attrbute_to_static_str(
.or_else(|| {
ELEMENTS_WITH_NAMESPACE.iter().find_map(|(el, ns, attrs)| {
(element == *el && namespace == Some(*ns))
.then(|| attrs.iter().find(|a| **a == attr).map(|a| (*a, None)))
.then(|| {
attrs
.iter()
.find(|a| **a == attr)
.map(|a| (*a, None, false))
})
.flatten()
})
})
.or_else(|| {
ELEMENTS_WITHOUT_NAMESPACE.iter().find_map(|(el, attrs)| {
(element == *el)
.then(|| attrs.iter().find(|a| **a == attr).map(|a| (*a, None)))
.then(|| {
attrs
.iter()
.find(|a| **a == attr)
.map(|a| (*a, None, false))
})
.flatten()
})
})
@ -628,7 +638,6 @@ mapped_trait_methods! {
}
pub mod svg {
mapped_trait_methods! {
accent_height: "accent-height",
accumulate: "accumulate",

View file

@ -23,7 +23,7 @@ use syn::{
Token,
};
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct Component {
pub name: syn::Path,
pub prop_gen_args: Option<AngleBracketedGenericArguments>,
@ -174,7 +174,11 @@ impl ToTokens for Component {
let key_token = match has_key {
Some(field) => {
let inners = &field.content;
quote! { Some(format_args_f!(#inners)) }
if let ContentField::Formatted(ifmt) = inners {
quote! { Some(#ifmt) }
} else {
unreachable!()
}
}
None => quote! { None },
};
@ -193,16 +197,16 @@ impl ToTokens for Component {
}
// the struct's fields info
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct ComponentField {
pub name: Ident,
pub content: ContentField,
}
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub enum ContentField {
ManExpr(Expr),
Formatted(LitStr),
Formatted(IfmtInput),
OnHandlerRaw(Expr),
}
@ -211,7 +215,7 @@ impl ToTokens for ContentField {
match self {
ContentField::ManExpr(e) => e.to_tokens(tokens),
ContentField::Formatted(s) => tokens.append_all(quote! {
__cx.raw_text(format_args_f!(#s)).0
__cx.raw_text(#s).0
}),
ContentField::OnHandlerRaw(e) => tokens.append_all(quote! {
__cx.event_handler(#e)
@ -231,7 +235,7 @@ impl Parse for ComponentField {
}
if name == "key" {
let content = ContentField::ManExpr(input.parse()?);
let content = ContentField::Formatted(input.parse()?);
return Ok(Self { name, content });
}
if input.peek(LitStr) {

View file

@ -10,10 +10,10 @@ use syn::{
// =======================================
// Parse the VNode::Element type
// =======================================
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct Element {
pub name: Ident,
pub key: Option<LitStr>,
pub key: Option<IfmtInput>,
pub attributes: Vec<ElementAttrNamed>,
pub children: Vec<BodyNode>,
pub _is_static: bool,
@ -46,7 +46,7 @@ impl Parse for Element {
content.parse::<Token![:]>()?;
if content.peek(LitStr) && content.peek2(Token![,]) {
let value = content.parse::<LitStr>()?;
let value = content.parse()?;
attributes.push(ElementAttrNamed {
el_name: el_name.clone(),
attr: ElementAttr::CustomAttrText { name, value },
@ -164,7 +164,7 @@ impl ToTokens for Element {
let children = &self.children;
let key = match &self.key {
Some(ty) => quote! { Some(format_args_f!(#ty)) },
Some(ty) => quote! { Some(#ty) },
None => quote! { None },
};
@ -190,16 +190,16 @@ impl ToTokens for Element {
}
}
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub enum ElementAttr {
/// attribute: "valuee {}"
AttrText { name: Ident, value: LitStr },
AttrText { name: Ident, value: IfmtInput },
/// attribute: true,
AttrExpression { name: Ident, value: Expr },
/// "attribute": "value {}"
CustomAttrText { name: LitStr, value: LitStr },
CustomAttrText { name: LitStr, value: IfmtInput },
/// "attribute": true,
CustomAttrExpression { name: LitStr, value: Expr },
@ -231,7 +231,7 @@ impl ElementAttr {
}
}
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub struct ElementAttrNamed {
pub el_name: Ident,
pub attr: ElementAttr,
@ -244,29 +244,24 @@ impl ToTokens for ElementAttrNamed {
tokens.append_all(match attr {
ElementAttr::AttrText { name, value } => {
quote! {
dioxus_elements::#el_name.#name(__cx, format_args_f!(#value))
__cx.attr_disciption( dioxus_elements::#el_name::#name, #value)
}
}
ElementAttr::AttrExpression { name, value } => {
quote! {
dioxus_elements::#el_name.#name(__cx, #value)
__cx.attr_disciption( dioxus_elements::#el_name::#name, #value)
}
}
ElementAttr::CustomAttrText { name, value } => {
quote! {
__cx.attr( #name, format_args_f!(#value), None, false )
__cx.attr( #name, #value, None, false )
}
}
ElementAttr::CustomAttrExpression { name, value } => {
quote! {
__cx.attr( #name, format_args_f!(#value), None, false )
__cx.attr( #name, #value, None, false )
}
}
// ElementAttr::EventClosure { name, closure } => {
// quote! {
// dioxus_elements::on::#name(__cx, #closure)
// }
// }
ElementAttr::EventTokens { name, tokens } => {
quote! {
dioxus_elements::on::#name(__cx, #tokens)

View file

@ -65,14 +65,22 @@ macro_rules! builder_constructors {
];
};
}
pub const ELEMENTS_WITH_MAPPED_ATTRIBUTES: &[(&str, &[(&str, &str)])] = &[
("script", &[("r#type", "type"), ("r#script", "script")]),
("button", &[("r#type", "type")]),
("select", &[("value", "value")]),
("option", &[("selected", "selected")]),
("textarea", &[("value", "value")]),
("label", &[("r#for", "for")]),
("input", &[("r#type", "type"), ("value", "value")]),
/// All attributes that are tied to a specific element that either have a different name, or are volitile
pub const ELEMENTS_WITH_MAPPED_ATTRIBUTES: &[(&str, &[(&str, &str, bool)])] = &[
(
"script",
&[("r#type", "type", false), ("r#script", "script", false)],
),
("button", &[("r#type", "type", false)]),
("select", &[("value", "value", true)]),
("option", &[("selected", "selected", true)]),
("textarea", &[("value", "value", true)]),
("label", &[("r#for", "for", false)]),
(
"input",
&[("r#type", "type", false), ("value", "value", true)],
),
];
// Organized in the same order as

View file

@ -1,9 +1,8 @@
use std::fmt::Display;
use dioxus_core::OwnedCodeLocation;
use serde::{Deserialize, Serialize};
use crate::CodeLocation;
/// An error produced when interperting the rsx
#[derive(Debug, Serialize, Deserialize)]
pub enum Error {
@ -23,11 +22,11 @@ pub enum RecompileReason {
#[derive(Debug, Serialize, Deserialize)]
pub struct ParseError {
pub message: String,
pub location: CodeLocation,
pub location: OwnedCodeLocation,
}
impl ParseError {
pub fn new(error: syn::Error, mut location: CodeLocation) -> Self {
pub fn new(error: syn::Error, mut location: OwnedCodeLocation) -> Self {
let message = error.to_string();
let syn_call_site = error.span().start();
location.line += syn_call_site.line as u32;

View file

@ -2,87 +2,37 @@ use std::{collections::HashSet, str::FromStr};
use proc_macro2::{Span, TokenStream};
use quote::{quote, ToTokens};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{
parse::{Parse, ParseStream},
*,
};
pub fn format_args_f_impl(input: IfmtInput) -> Result<TokenStream> {
// build format_literal
let mut format_literal = String::new();
let mut expr_counter = 0;
for segment in input.segments.iter() {
match segment {
Segment::Literal(s) => format_literal += &s.replace('{', "{{").replace('}', "}}"),
Segment::Formatted {
format_args,
segment,
} => {
format_literal += "{";
match segment {
FormattedSegment::Expr(_) => {
format_literal += &expr_counter.to_string();
expr_counter += 1;
}
FormattedSegment::Ident(ident) => {
format_literal += &ident.to_string();
}
}
format_literal += ":";
format_literal += format_args;
format_literal += "}";
}
}
}
let positional_args = input.segments.iter().filter_map(|seg| {
if let Segment::Formatted {
segment: FormattedSegment::Expr(expr),
..
} = seg
{
Some(expr)
} else {
None
}
});
// remove duplicate idents
let named_args_idents: HashSet<_> = input
.segments
.iter()
.filter_map(|seg| {
if let Segment::Formatted {
segment: FormattedSegment::Ident(ident),
..
} = seg
{
Some(ident)
} else {
None
}
})
.collect();
let named_args = named_args_idents
.iter()
.map(|ident| quote!(#ident = #ident));
Ok(quote! {
format_args!(
#format_literal
#(, #positional_args)*
#(, #named_args)*
)
})
Ok(input.into_token_stream())
}
#[allow(dead_code)] // dumb compiler does not see the struct being used...
#[derive(Debug)]
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct IfmtInput {
pub source: Option<LitStr>,
pub segments: Vec<Segment>,
}
impl IfmtInput {
pub fn to_static(&self) -> Option<String> {
self.segments
.iter()
.try_fold(String::new(), |acc, segment| {
if let Segment::Literal(seg) = segment {
Some(acc + seg)
} else {
None
}
})
}
}
impl FromStr for IfmtInput {
type Err = syn::Error;
@ -96,7 +46,9 @@ impl FromStr for IfmtInput {
current_literal.push(c);
continue;
}
segments.push(Segment::Literal(current_literal));
if !current_literal.is_empty() {
segments.push(Segment::Literal(current_literal));
}
current_literal = String::new();
let mut current_captured = String::new();
while let Some(c) = chars.next() {
@ -104,10 +56,10 @@ impl FromStr for IfmtInput {
let mut current_format_args = String::new();
for c in chars.by_ref() {
if c == '}' {
segments.push(Segment::Formatted {
segments.push(Segment::Formatted(FormattedSegment {
format_args: current_format_args,
segment: FormattedSegment::parse(&current_captured)?,
});
segment: FormattedSegmentType::parse(&current_captured)?,
}));
break;
}
current_format_args.push(c);
@ -115,10 +67,10 @@ impl FromStr for IfmtInput {
break;
}
if c == '}' {
segments.push(Segment::Formatted {
segments.push(Segment::Formatted(FormattedSegment {
format_args: String::new(),
segment: FormattedSegment::parse(&current_captured)?,
});
segment: FormattedSegmentType::parse(&current_captured)?,
}));
break;
}
current_captured.push(c);
@ -138,27 +90,117 @@ impl FromStr for IfmtInput {
current_literal.push(c);
}
}
segments.push(Segment::Literal(current_literal));
Ok(Self { segments })
if !current_literal.is_empty() {
segments.push(Segment::Literal(current_literal));
}
Ok(Self {
segments,
source: None,
})
}
}
#[derive(Debug)]
pub enum Segment {
Literal(String),
Formatted {
format_args: String,
segment: FormattedSegment,
},
impl ToTokens for IfmtInput {
fn to_tokens(&self, tokens: &mut TokenStream) {
// build format_literal
let mut format_literal = String::new();
let mut expr_counter = 0;
for segment in self.segments.iter() {
match segment {
Segment::Literal(s) => format_literal += &s.replace('{', "{{").replace('}', "}}"),
Segment::Formatted(FormattedSegment {
format_args,
segment,
}) => {
format_literal += "{";
match segment {
FormattedSegmentType::Expr(_) => {
format_literal += &expr_counter.to_string();
expr_counter += 1;
}
FormattedSegmentType::Ident(ident) => {
format_literal += &ident.to_string();
}
}
format_literal += ":";
format_literal += format_args;
format_literal += "}";
}
}
}
let positional_args = self.segments.iter().filter_map(|seg| {
if let Segment::Formatted(FormattedSegment {
segment: FormattedSegmentType::Expr(expr),
..
}) = seg
{
Some(expr)
} else {
None
}
});
// remove duplicate idents
let named_args_idents: HashSet<_> = self
.segments
.iter()
.filter_map(|seg| {
if let Segment::Formatted(FormattedSegment {
segment: FormattedSegmentType::Ident(ident),
..
}) = seg
{
Some(ident)
} else {
None
}
})
.collect();
let named_args = named_args_idents
.iter()
.map(|ident| quote!(#ident = #ident));
quote! {
format_args!(
#format_literal
#(, #positional_args)*
#(, #named_args)*
)
}
.to_tokens(tokens)
}
}
#[derive(Debug)]
pub enum FormattedSegment {
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub enum Segment {
Literal(String),
Formatted(FormattedSegment),
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub struct FormattedSegment {
format_args: String,
segment: FormattedSegmentType,
}
impl ToTokens for FormattedSegment {
fn to_tokens(&self, tokens: &mut TokenStream) {
let (fmt, seg) = (&self.format_args, &self.segment);
let fmt = format!("{{0:{}}}", fmt);
tokens.append_all(quote! {
format_args!(#fmt, #seg)
});
}
}
#[derive(Debug, PartialEq, Eq, Clone, Hash)]
pub enum FormattedSegmentType {
Expr(Box<Expr>),
Ident(Ident),
}
impl FormattedSegment {
impl FormattedSegmentType {
fn parse(input: &str) -> Result<Self> {
if let Ok(ident) = parse_str::<Ident>(input) {
if ident == input {
@ -176,7 +218,7 @@ impl FormattedSegment {
}
}
impl ToTokens for FormattedSegment {
impl ToTokens for FormattedSegmentType {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
Self::Expr(expr) => expr.to_tokens(tokens),
@ -189,6 +231,8 @@ impl Parse for IfmtInput {
fn parse(input: ParseStream) -> Result<Self> {
let input: LitStr = input.parse()?;
let input_str = input.value();
IfmtInput::from_str(&input_str)
let mut ifmt = IfmtInput::from_str(&input_str)?;
ifmt.source = Some(input);
Ok(ifmt)
}
}

View file

@ -13,17 +13,25 @@
#[macro_use]
mod errors;
#[cfg(any(feature = "hot-reload", debug_assertions))]
mod attributes;
mod component;
mod element;
#[cfg(any(feature = "hot-reload", debug_assertions))]
mod elements;
#[cfg(any(feature = "hot-reload", debug_assertions))]
mod error;
mod ifmt;
mod node;
mod template;
// Re-export the namespaces into each other
pub use component::*;
pub use element::*;
pub use ifmt::*;
pub use node::*;
#[cfg(any(feature = "hot-reload", debug_assertions))]
pub use template::{try_parse_template, DynamicTemplateContextBuilder};
// imports
use proc_macro2::TokenStream as TokenStream2;
@ -32,6 +40,7 @@ use syn::{
parse::{Parse, ParseStream},
Result, Token,
};
use template::TemplateBuilder;
pub struct CallBody {
pub roots: Vec<BodyNode>,
@ -58,12 +67,17 @@ impl Parse for CallBody {
/// Serialize the same way, regardless of flavor
impl ToTokens for CallBody {
fn to_tokens(&self, out_tokens: &mut TokenStream2) {
let inner = if self.roots.len() == 1 {
let inner = &self.roots[0];
quote! { #inner }
let template = TemplateBuilder::from_roots(self.roots.clone());
let inner = if let Some(template) = template {
quote! { #template }
} else {
let childs = &self.roots;
quote! { __cx.fragment_root([ #(#childs),* ]) }
let children = &self.roots;
if children.len() == 1 {
let inner = &self.roots[0];
quote! { #inner }
} else {
quote! { __cx.fragment_root([ #(#children),* ]) }
}
};
// Otherwise we just build the LazyNode wrapper
@ -75,3 +89,22 @@ impl ToTokens for CallBody {
})
}
}
impl CallBody {
pub fn to_tokens_without_template(&self, out_tokens: &mut TokenStream2) {
let children = &self.roots;
let inner = if children.len() == 1 {
let inner = &self.roots[0];
quote! { #inner }
} else {
quote! { __cx.fragment_root([ #(#children),* ]) }
};
out_tokens.append_all(quote! {
LazyNodes::new(move |__cx: NodeFactory| -> VNode {
use dioxus_elements::{GlobalAttributes, SvgAttributes};
#inner
})
})
}
}

View file

@ -16,11 +16,11 @@ Parse
-> "text {with_args}"
-> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr
*/
#[derive(PartialEq, Eq)]
#[derive(PartialEq, Eq, Clone, Debug, Hash)]
pub enum BodyNode {
Element(Element),
Component(Component),
Text(LitStr),
Text(IfmtInput),
RawExpr(Expr),
}
@ -33,7 +33,7 @@ impl BodyNode {
match self {
BodyNode::Element(el) => el.name.span(),
BodyNode::Component(component) => component.name.span(),
BodyNode::Text(text) => text.span(),
BodyNode::Text(text) => text.source.span(),
BodyNode::RawExpr(exp) => exp.span(),
}
}
@ -98,7 +98,7 @@ impl ToTokens for BodyNode {
BodyNode::Element(el) => el.to_tokens(tokens),
BodyNode::Component(comp) => comp.to_tokens(tokens),
BodyNode::Text(txt) => tokens.append_all(quote! {
__cx.text(format_args_f!(#txt))
__cx.text(#txt)
}),
BodyNode::RawExpr(exp) => tokens.append_all(quote! {
__cx.fragment_from_iter(#exp)

View file

@ -0,0 +1,917 @@
use dioxus_core::{
OwnedAttributeValue, TemplateAttributeValue, TemplateNodeId, TextTemplate, TextTemplateSegment,
};
use proc_macro2::TokenStream;
use quote::TokenStreamExt;
use quote::{quote, ToTokens};
use syn::{Expr, Ident, LitStr};
#[cfg(any(feature = "hot-reload", debug_assertions))]
pub fn try_parse_template(
rsx: &str,
location: OwnedCodeLocation,
previous_template: Option<DynamicTemplateContextBuilder>,
) -> Result<(OwnedTemplate, DynamicTemplateContextBuilder), Error> {
use crate::CallBody;
let call_body: CallBody =
syn::parse_str(rsx).map_err(|e| Error::ParseError(ParseError::new(e, location.clone())))?;
let mut template_builder = TemplateBuilder::from_roots_always(call_body.roots);
if let Some(prev) = previous_template {
template_builder = template_builder
.try_switch_dynamic_context(prev)
.ok_or_else(|| {
Error::RecompileRequiredError(RecompileReason::CapturedVariable(
"dynamic context updated".to_string(),
))
})?;
}
let dyn_ctx = template_builder.dynamic_context.clone();
Ok((template_builder.try_into_owned(&location)?, dyn_ctx))
}
#[cfg(any(feature = "hot-reload", debug_assertions))]
use hot_reload_imports::*;
#[cfg(any(feature = "hot-reload", debug_assertions))]
mod hot_reload_imports {
pub use crate::{
attributes::attrbute_to_static_str,
elements::element_to_static_str,
error::{Error, ParseError, RecompileReason},
};
pub use dioxus_core::prelude::OwnedTemplate;
pub use dioxus_core::{
AttributeDiscription, OwnedAttributeValue, OwnedCodeLocation, OwnedDynamicNodeMapping,
OwnedTemplateNode, Template, TemplateAttribute, TemplateAttributeValue, TemplateElement,
TemplateNodeId, TemplateNodeType, TextTemplate, TextTemplateSegment,
};
pub use std::collections::HashMap;
}
use crate::{BodyNode, ElementAttr, FormattedSegment, Segment};
struct TemplateElementBuilder {
tag: Ident,
attributes: Vec<TemplateAttributeBuilder>,
children: Vec<TemplateNodeId>,
listeners: Vec<usize>,
parent: Option<TemplateNodeId>,
}
impl TemplateElementBuilder {
#[cfg(any(feature = "hot-reload", debug_assertions))]
fn try_into_owned(
self,
location: &OwnedCodeLocation,
) -> Result<
TemplateElement<
Vec<TemplateAttribute<OwnedAttributeValue>>,
OwnedAttributeValue,
Vec<TemplateNodeId>,
Vec<usize>,
>,
Error,
> {
let Self {
tag,
attributes,
children,
listeners,
parent,
} = self;
let (element_tag, element_ns) =
element_to_static_str(&tag.to_string()).ok_or_else(|| {
Error::ParseError(ParseError::new(
syn::Error::new(tag.span(), format!("unknown element: {}", tag)),
location.clone(),
))
})?;
let mut owned_attributes = Vec::new();
for a in attributes {
owned_attributes.push(a.try_into_owned(location, element_tag, element_ns)?);
}
Ok(TemplateElement::new(
element_tag,
element_ns,
owned_attributes,
children,
listeners,
parent,
))
}
}
impl ToTokens for TemplateElementBuilder {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
tag,
attributes,
children,
listeners,
parent,
} = self;
let children = children.iter().map(|id| {
let raw = id.0;
quote! {TemplateNodeId(#raw)}
});
let parent = match parent {
Some(id) => {
let raw = id.0;
quote! {Some(TemplateNodeId(#raw))}
}
None => quote! {None},
};
tokens.append_all(quote! {
TemplateElement::new(
dioxus_elements::#tag::TAG_NAME,
dioxus_elements::#tag::NAME_SPACE,
&[#(#attributes),*],
&[#(#children),*],
&[#(#listeners),*],
#parent,
)
})
}
}
enum AttributeName {
Ident(Ident),
Str(LitStr),
}
struct TemplateAttributeBuilder {
element_tag: Ident,
name: AttributeName,
value: TemplateAttributeValue<OwnedAttributeValue>,
}
impl TemplateAttributeBuilder {
#[cfg(any(feature = "hot-reload", debug_assertions))]
fn try_into_owned(
self,
location: &OwnedCodeLocation,
element_tag: &'static str,
element_ns: Option<&'static str>,
) -> Result<TemplateAttribute<OwnedAttributeValue>, Error> {
let Self { name, value, .. } = self;
let (name, span, literal) = match name {
AttributeName::Ident(name) => (name.to_string(), name.span(), false),
AttributeName::Str(name) => (name.value(), name.span(), true),
};
let (name, namespace, volatile) = attrbute_to_static_str(&name, element_tag, element_ns)
.ok_or_else(|| {
if literal {
// literals will be captured when a full recompile is triggered
Error::RecompileRequiredError(RecompileReason::CapturedAttribute(
name.to_string(),
))
} else {
Error::ParseError(ParseError::new(
syn::Error::new(span, format!("unknown attribute: {}", name)),
location.clone(),
))
}
})?;
let attribute = AttributeDiscription {
name,
namespace,
volatile,
};
Ok(TemplateAttribute { value, attribute })
}
}
impl ToTokens for TemplateAttributeBuilder {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
element_tag,
name,
value,
} = self;
let value = match value {
TemplateAttributeValue::Static(val) => {
let val = match val {
OwnedAttributeValue::Text(txt) => quote! {StaticAttributeValue::Text(#txt)},
OwnedAttributeValue::Float32(f) => quote! {StaticAttributeValue::Float32(#f)},
OwnedAttributeValue::Float64(f) => quote! {StaticAttributeValue::Float64(#f)},
OwnedAttributeValue::Int32(i) => quote! {StaticAttributeValue::Int32(#i)},
OwnedAttributeValue::Int64(i) => quote! {StaticAttributeValue::Int64(#i)},
OwnedAttributeValue::Uint32(u) => quote! {StaticAttributeValue::Uint32(#u)},
OwnedAttributeValue::Uint64(u) => quote! {StaticAttributeValue::Uint64(#u)},
OwnedAttributeValue::Bool(b) => quote! {StaticAttributeValue::Bool(#b)},
OwnedAttributeValue::Vec3Float(f1, f2, f3) => {
quote! {StaticAttributeValue::Vec3Float(#f1, #f2, #f3)}
}
OwnedAttributeValue::Vec3Int(f1, f2, f3) => {
quote! {StaticAttributeValue::Vec3Int(#f1, #f2, #f3)}
}
OwnedAttributeValue::Vec3Uint(f1, f2, f3) => {
quote! {StaticAttributeValue::Vec3Uint(#f1, #f2, #f3)}
}
OwnedAttributeValue::Vec4Float(f1, f2, f3, f4) => {
quote! {StaticAttributeValue::Vec4Float(#f1, #f2, #f3, #f4)}
}
OwnedAttributeValue::Vec4Int(f1, f2, f3, f4) => {
quote! {StaticAttributeValue::Vec4Int(#f1, #f2, #f3, #f4)}
}
OwnedAttributeValue::Vec4Uint(f1, f2, f3, f4) => {
quote! {StaticAttributeValue::Vec4Uint(#f1, #f2, #f3, #f4)}
}
OwnedAttributeValue::Bytes(b) => {
quote! {StaticAttributeValue::Bytes(&[#(#b),*])}
}
};
quote! {TemplateAttributeValue::Static(#val)}
}
TemplateAttributeValue::Dynamic(idx) => quote! {TemplateAttributeValue::Dynamic(#idx)},
};
match name {
AttributeName::Ident(name) => tokens.append_all(quote! {
TemplateAttribute{
attribute: dioxus_elements::#element_tag::#name,
value: #value,
}
}),
AttributeName::Str(lit) => tokens.append_all(quote! {
TemplateAttribute{
attribute: dioxus::prelude::AttributeDiscription{
name: #lit,
namespace: None,
volatile: false
},
value: #value,
}
}),
}
}
}
enum TemplateNodeTypeBuilder {
Element(TemplateElementBuilder),
Text(TextTemplate<Vec<TextTemplateSegment<String>>, String>),
DynamicNode(usize),
}
impl TemplateNodeTypeBuilder {
#[cfg(any(feature = "hot-reload", debug_assertions))]
fn try_into_owned(
self,
location: &OwnedCodeLocation,
) -> Result<
TemplateNodeType<
Vec<TemplateAttribute<OwnedAttributeValue>>,
OwnedAttributeValue,
Vec<TemplateNodeId>,
Vec<usize>,
Vec<TextTemplateSegment<String>>,
String,
>,
Error,
> {
match self {
TemplateNodeTypeBuilder::Element(el) => {
Ok(TemplateNodeType::Element(el.try_into_owned(location)?))
}
TemplateNodeTypeBuilder::Text(txt) => Ok(TemplateNodeType::Text(txt)),
TemplateNodeTypeBuilder::DynamicNode(idx) => Ok(TemplateNodeType::DynamicNode(idx)),
}
}
}
impl ToTokens for TemplateNodeTypeBuilder {
fn to_tokens(&self, tokens: &mut TokenStream) {
match self {
TemplateNodeTypeBuilder::Element(el) => tokens.append_all(quote! {
TemplateNodeType::Element(#el)
}),
TemplateNodeTypeBuilder::Text(txt) => {
let segments = txt.segments.iter().map(|seg| match seg {
TextTemplateSegment::Static(s) => quote!(TextTemplateSegment::Static(#s)),
TextTemplateSegment::Dynamic(idx) => quote!(TextTemplateSegment::Dynamic(#idx)),
});
tokens.append_all(quote! {
TemplateNodeType::Text(TextTemplate::new(&[#(#segments),*]))
});
}
TemplateNodeTypeBuilder::DynamicNode(idx) => tokens.append_all(quote! {
TemplateNodeType::DynamicNode(#idx)
}),
}
}
}
struct TemplateNodeBuilder {
id: TemplateNodeId,
node_type: TemplateNodeTypeBuilder,
}
impl TemplateNodeBuilder {
#[cfg(any(feature = "hot-reload", debug_assertions))]
fn try_into_owned(self, location: &OwnedCodeLocation) -> Result<OwnedTemplateNode, Error> {
let TemplateNodeBuilder { id, node_type } = self;
let node_type = node_type.try_into_owned(location)?;
Ok(OwnedTemplateNode {
id,
node_type,
locally_static: false,
fully_static: false,
})
}
fn is_fully_static(&self, nodes: &Vec<TemplateNodeBuilder>) -> bool {
self.is_locally_static()
&& match &self.node_type {
TemplateNodeTypeBuilder::Element(el) => el
.children
.iter()
.all(|child| nodes[child.0].is_fully_static(nodes)),
TemplateNodeTypeBuilder::Text(_) => true,
TemplateNodeTypeBuilder::DynamicNode(_) => unreachable!(),
}
}
fn is_locally_static(&self) -> bool {
match &self.node_type {
TemplateNodeTypeBuilder::Element(el) => {
el.attributes.iter().all(|attr| match &attr.value {
TemplateAttributeValue::Static(_) => true,
TemplateAttributeValue::Dynamic(_) => false,
}) && el.listeners.is_empty()
}
TemplateNodeTypeBuilder::Text(txt) => txt.segments.iter().all(|seg| match seg {
TextTemplateSegment::Static(_) => true,
TextTemplateSegment::Dynamic(_) => false,
}),
TemplateNodeTypeBuilder::DynamicNode(_) => false,
}
}
fn to_tokens(&self, tokens: &mut TokenStream, nodes: &Vec<TemplateNodeBuilder>) {
let Self { id, node_type } = self;
let raw_id = id.0;
let fully_static = self.is_fully_static(nodes);
let locally_static = self.is_locally_static();
tokens.append_all(quote! {
TemplateNode {
id: TemplateNodeId(#raw_id),
node_type: #node_type,
locally_static: #locally_static,
fully_static: #fully_static,
}
})
}
}
#[derive(Default)]
pub struct TemplateBuilder {
nodes: Vec<TemplateNodeBuilder>,
root_nodes: Vec<TemplateNodeId>,
dynamic_context: DynamicTemplateContextBuilder,
}
impl TemplateBuilder {
/// Create a template builder from nodes if it would improve performance to do so.
pub fn from_roots(roots: Vec<BodyNode>) -> Option<Self> {
let mut builder = Self::default();
for root in roots {
let id = builder.build_node(root, None);
builder.root_nodes.push(id);
}
// only build a template if there is at least one static node
if builder.nodes.iter().all(|r| {
if let TemplateNodeTypeBuilder::DynamicNode(_) = &r.node_type {
true
} else {
false
}
}) {
None
} else {
Some(builder)
}
}
/// Create a template builder from nodes regardless of performance.
#[cfg(any(feature = "hot-reload", debug_assertions))]
fn from_roots_always(roots: Vec<BodyNode>) -> Self {
let mut builder = Self::default();
for root in roots {
let id = builder.build_node(root, None);
builder.root_nodes.push(id);
}
builder
}
fn build_node(&mut self, node: BodyNode, parent: Option<TemplateNodeId>) -> TemplateNodeId {
let id = TemplateNodeId(self.nodes.len());
match node {
BodyNode::Element(el) => {
let mut attributes = Vec::new();
let mut listeners = Vec::new();
for attr in el.attributes {
match attr.attr {
ElementAttr::AttrText { name, value } => {
if let Some(static_value) = value.to_static() {
attributes.push(TemplateAttributeBuilder {
element_tag: el.name.clone(),
name: AttributeName::Ident(name),
value: TemplateAttributeValue::Static(
OwnedAttributeValue::Text(static_value),
),
})
} else {
attributes.push(TemplateAttributeBuilder {
element_tag: el.name.clone(),
name: AttributeName::Ident(name),
value: TemplateAttributeValue::Dynamic(
self.dynamic_context.add_attr(quote!(AttributeValue::Text(
dioxus::core::exports::bumpalo::format!(in __bump, "{}", #value)
.into_bump_str()
))),
),
})
}
}
ElementAttr::CustomAttrText { name, value } => {
if let Some(static_value) = value.to_static() {
attributes.push(TemplateAttributeBuilder {
element_tag: el.name.clone(),
name: AttributeName::Str(name),
value: TemplateAttributeValue::Static(
OwnedAttributeValue::Text(static_value),
),
})
} else {
attributes.push(TemplateAttributeBuilder {
element_tag: el.name.clone(),
name: AttributeName::Str(name),
value: TemplateAttributeValue::Dynamic(
self.dynamic_context.add_attr(quote!(AttributeValue::Text(
dioxus::core::exports::bumpalo::format!(in __bump, "{}", #value)
.into_bump_str()
))),
),
})
}
}
ElementAttr::AttrExpression { name, value } => {
attributes.push(TemplateAttributeBuilder {
element_tag: el.name.clone(),
name: AttributeName::Ident(name),
value: TemplateAttributeValue::Dynamic(
self.dynamic_context.add_attr(quote!(AttributeValue::Text(
dioxus::core::exports::bumpalo::format!(in __bump, "{}", #value)
.into_bump_str()
))),
),
})
}
ElementAttr::CustomAttrExpression { name, value } => {
attributes.push(TemplateAttributeBuilder {
element_tag: el.name.clone(),
name: AttributeName::Str(name),
value: TemplateAttributeValue::Dynamic(
self.dynamic_context.add_attr(quote!(AttributeValue::Text(
dioxus::core::exports::bumpalo::format!(in __bump, "{}", #value)
.into_bump_str()
))),
),
})
}
ElementAttr::EventTokens { name, tokens } => {
listeners.push(self.dynamic_context.add_listener(name, tokens))
}
}
}
if let Some(key) = el.key {
self.dynamic_context.add_key(quote!(
dioxus::core::exports::bumpalo::format!(in __bump, "{}", #key)
.into_bump_str()
));
}
self.nodes.push(TemplateNodeBuilder {
id,
node_type: TemplateNodeTypeBuilder::Element(TemplateElementBuilder {
tag: el.name,
attributes,
children: Vec::new(),
listeners,
parent,
}),
});
let children: Vec<_> = el
.children
.into_iter()
.map(|child| self.build_node(child, Some(id)))
.collect();
let parent = &mut self.nodes[id.0];
if let TemplateNodeTypeBuilder::Element(element) = &mut parent.node_type {
element.children = children;
}
}
BodyNode::Component(comp) => {
self.nodes.push(TemplateNodeBuilder {
id,
node_type: TemplateNodeTypeBuilder::DynamicNode(
self.dynamic_context.add_node(BodyNode::Component(comp)),
),
});
}
BodyNode::Text(txt) => {
let mut segments = Vec::new();
for segment in txt.segments {
segments.push(match segment {
Segment::Literal(lit) => TextTemplateSegment::Static(lit),
Segment::Formatted(fmted) => {
TextTemplateSegment::Dynamic(self.dynamic_context.add_text(fmted))
}
})
}
self.nodes.push(TemplateNodeBuilder {
id,
node_type: TemplateNodeTypeBuilder::Text(TextTemplate::new(segments)),
});
}
BodyNode::RawExpr(expr) => {
self.nodes.push(TemplateNodeBuilder {
id,
node_type: TemplateNodeTypeBuilder::DynamicNode(
self.dynamic_context.add_node(BodyNode::RawExpr(expr)),
),
});
}
}
id
}
#[cfg(any(feature = "hot-reload", debug_assertions))]
pub fn try_switch_dynamic_context(
mut self,
dynamic_context: DynamicTemplateContextBuilder,
) -> Option<Self> {
let attribute_mapping: HashMap<String, usize> = dynamic_context
.attributes
.iter()
.enumerate()
.map(|(i, ts)| (ts.to_string(), i))
.collect();
let text_mapping: HashMap<String, usize> = dynamic_context
.text
.iter()
.enumerate()
.map(|(i, ts)| (ts.to_token_stream().to_string(), i))
.collect();
let listener_mapping: HashMap<(String, Expr), usize> = dynamic_context
.listeners
.iter()
.enumerate()
.map(|(i, ts)| (ts.clone(), i))
.collect();
let node_mapping: HashMap<String, usize> = dynamic_context
.nodes
.iter()
.enumerate()
.map(|(i, ts)| (ts.to_token_stream().to_string(), i))
.collect();
for node in &mut self.nodes {
match &mut node.node_type {
TemplateNodeTypeBuilder::Element(element) => {
for listener in &mut element.listeners {
*listener =
*listener_mapping.get(&self.dynamic_context.listeners[*listener])?;
}
for attribute in &mut element.attributes {
if let TemplateAttributeValue::Dynamic(idx) = &mut attribute.value {
*idx = *attribute_mapping
.get(&self.dynamic_context.attributes[*idx].to_string())?;
}
}
}
TemplateNodeTypeBuilder::Text(txt) => {
for seg in &mut txt.segments {
if let TextTemplateSegment::Dynamic(idx) = seg {
*idx = *text_mapping.get(
&self.dynamic_context.text[*idx]
.to_token_stream()
.to_string(),
)?;
}
}
}
TemplateNodeTypeBuilder::DynamicNode(idx) => {
*idx = *node_mapping.get(
&self.dynamic_context.nodes[*idx]
.to_token_stream()
.to_string(),
)?;
}
}
}
self.dynamic_context = dynamic_context;
Some(self)
}
#[cfg(any(feature = "hot-reload", debug_assertions))]
pub fn try_into_owned(self, location: &OwnedCodeLocation) -> Result<OwnedTemplate, Error> {
let mut nodes = Vec::new();
let dynamic_mapping = self.dynamic_mapping(&nodes);
for node in self.nodes {
nodes.push(node.try_into_owned(location)?);
}
Ok(OwnedTemplate {
nodes,
root_nodes: self.root_nodes,
dynamic_mapping,
})
}
#[cfg(any(feature = "hot-reload", debug_assertions))]
pub fn dynamic_mapping(
&self,
resolved_nodes: &Vec<OwnedTemplateNode>,
) -> OwnedDynamicNodeMapping {
let dynamic_context = &self.dynamic_context;
let mut node_mapping = vec![None; dynamic_context.nodes.len()];
let nodes = &self.nodes;
for n in nodes {
match &n.node_type {
TemplateNodeTypeBuilder::DynamicNode(idx) => node_mapping[*idx] = Some(n.id),
_ => (),
}
}
let mut text_mapping = vec![Vec::new(); dynamic_context.text.len()];
for n in nodes {
match &n.node_type {
TemplateNodeTypeBuilder::Text(txt) => {
for seg in &txt.segments {
match seg {
TextTemplateSegment::Static(_) => (),
TextTemplateSegment::Dynamic(idx) => text_mapping[*idx].push(n.id),
}
}
}
_ => (),
}
}
let mut attribute_mapping = vec![Vec::new(); dynamic_context.attributes.len()];
for n in nodes {
match &n.node_type {
TemplateNodeTypeBuilder::Element(el) => {
for (i, attr) in el.attributes.iter().enumerate() {
match attr.value {
TemplateAttributeValue::Static(_) => (),
TemplateAttributeValue::Dynamic(idx) => {
attribute_mapping[idx].push((n.id, i));
}
}
}
}
_ => (),
}
}
let mut listener_mapping = Vec::new();
for n in nodes {
match &n.node_type {
TemplateNodeTypeBuilder::Element(el) => {
if !el.listeners.is_empty() {
listener_mapping.push(n.id);
}
}
_ => (),
}
}
let mut volatile_mapping = Vec::new();
for n in resolved_nodes {
if let TemplateNodeType::Element(el) = &n.node_type {
for (i, attr) in el.attributes.iter().enumerate() {
if attr.attribute.volatile {
volatile_mapping.push((n.id, i));
}
}
}
}
OwnedDynamicNodeMapping::new(
node_mapping,
text_mapping,
attribute_mapping,
volatile_mapping,
listener_mapping,
)
}
}
impl ToTokens for TemplateBuilder {
fn to_tokens(&self, tokens: &mut TokenStream) {
let Self {
nodes,
root_nodes,
dynamic_context,
} = self;
let mut node_mapping = vec![None; dynamic_context.nodes.len()];
for n in nodes {
match &n.node_type {
TemplateNodeTypeBuilder::DynamicNode(idx) => node_mapping[*idx] = Some(n.id),
_ => (),
}
}
let mut text_mapping = vec![Vec::new(); dynamic_context.text.len()];
for n in nodes {
match &n.node_type {
TemplateNodeTypeBuilder::Text(txt) => {
for seg in &txt.segments {
match seg {
TextTemplateSegment::Static(_) => (),
TextTemplateSegment::Dynamic(idx) => text_mapping[*idx].push(n.id),
}
}
}
_ => (),
}
}
let mut attribute_mapping = vec![Vec::new(); dynamic_context.attributes.len()];
for n in nodes {
match &n.node_type {
TemplateNodeTypeBuilder::Element(el) => {
for (i, attr) in el.attributes.iter().enumerate() {
match attr.value {
TemplateAttributeValue::Static(_) => (),
TemplateAttributeValue::Dynamic(idx) => {
attribute_mapping[idx].push((n.id, i));
}
}
}
}
_ => (),
}
}
let mut listener_mapping = Vec::new();
for n in nodes {
match &n.node_type {
TemplateNodeTypeBuilder::Element(el) => {
if !el.listeners.is_empty() {
listener_mapping.push(n.id);
}
}
_ => (),
}
}
let root_nodes = root_nodes.iter().map(|id| {
let raw = id.0;
quote! { TemplateNodeId(#raw) }
});
let node_mapping_quoted = node_mapping.iter().map(|op| match op {
Some(id) => {
let raw_id = id.0;
quote! {Some(TemplateNodeId(#raw_id))}
}
None => quote! {None},
});
let text_mapping_quoted = text_mapping.iter().map(|inner| {
let raw = inner.iter().map(|id| id.0);
quote! {&[#(TemplateNodeId(#raw)),*]}
});
let attribute_mapping_quoted = attribute_mapping.iter().map(|inner| {
let raw = inner.iter().map(|(id, _)| id.0);
let indecies = inner.iter().map(|(_, idx)| idx);
quote! {&[#((TemplateNodeId(#raw), #indecies)),*]}
});
let listener_mapping_quoted = listener_mapping.iter().map(|id| {
let raw = id.0;
quote! {TemplateNodeId(#raw)}
});
let mut nodes_quoted = TokenStream::new();
for n in nodes {
n.to_tokens(&mut nodes_quoted, nodes);
quote! {,}.to_tokens(&mut nodes_quoted);
}
let quoted = quote! {
{
const __NODES: dioxus::prelude::StaticTemplateNodes = &[#nodes_quoted];
const __TEXT_MAPPING: &'static [&'static [dioxus::prelude::TemplateNodeId]] = &[#(#text_mapping_quoted),*];
const __ATTRIBUTE_MAPPING: &'static [&'static [(dioxus::prelude::TemplateNodeId, usize)]] = &[#(#attribute_mapping_quoted),*];
const __ROOT_NODES: &'static [dioxus::prelude::TemplateNodeId] = &[#(#root_nodes),*];
const __NODE_MAPPING: &'static [Option<dioxus::prelude::TemplateNodeId>] = &[#(#node_mapping_quoted),*];
const __NODES_WITH_LISTENERS: &'static [dioxus::prelude::TemplateNodeId] = &[#(#listener_mapping_quoted),*];
static __VOLITALE_MAPPING_INNER: dioxus::core::exports::once_cell::sync::Lazy<Vec<(dioxus::prelude::TemplateNodeId, usize)>> = dioxus::core::exports::once_cell::sync::Lazy::new(||{
// check each property to see if it is volatile
let mut volatile = Vec::new();
for n in __NODES {
if let TemplateNodeType::Element(el) = &n.node_type {
for (i, attr) in el.attributes.iter().enumerate() {
if attr.attribute.volatile {
volatile.push((n.id, i));
}
}
}
}
volatile
});
static __VOLITALE_MAPPING: &'static dioxus::core::exports::once_cell::sync::Lazy<Vec<(dioxus::prelude::TemplateNodeId, usize)>> = &__VOLITALE_MAPPING_INNER;
static __STATIC_VOLITALE_MAPPING: dioxus::prelude::LazyStaticVec<(dioxus::prelude::TemplateNodeId, usize)> = LazyStaticVec(__VOLITALE_MAPPING);
static __TEMPLATE: dioxus::prelude::Template = Template::Static(&StaticTemplate {
nodes: __NODES,
root_nodes: __ROOT_NODES,
dynamic_mapping: StaticDynamicNodeMapping::new(__NODE_MAPPING, __TEXT_MAPPING, __ATTRIBUTE_MAPPING, __STATIC_VOLITALE_MAPPING, __NODES_WITH_LISTENERS),
});
let __bump = __cx.bump();
__cx.template_ref(dioxus::prelude::TemplateId(get_line_num!()), __TEMPLATE.clone(), #dynamic_context)
}
};
tokens.append_all(quoted)
}
}
#[derive(Default, Clone, Debug)]
pub struct DynamicTemplateContextBuilder {
nodes: Vec<BodyNode>,
text: Vec<FormattedSegment>,
attributes: Vec<TokenStream>,
listeners: Vec<(String, Expr)>,
key: Option<TokenStream>,
}
impl DynamicTemplateContextBuilder {
fn add_node(&mut self, node: BodyNode) -> usize {
let node_id = self.nodes.len();
self.nodes.push(node);
node_id
}
fn add_text(&mut self, text: FormattedSegment) -> usize {
let text_id = self.text.len();
self.text.push(text);
text_id
}
fn add_attr(&mut self, attr: TokenStream) -> usize {
let attr_id = self.attributes.len();
self.attributes.push(attr);
attr_id
}
fn add_listener(&mut self, name: Ident, listener: Expr) -> usize {
let listener_id = self.listeners.len();
self.listeners.push((name.to_string(), listener));
listener_id
}
fn add_key(&mut self, key: TokenStream) {
self.key = Some(key);
}
}
impl ToTokens for DynamicTemplateContextBuilder {
fn to_tokens(&self, tokens: &mut TokenStream) {
let nodes = &self.nodes;
let text = &self.text;
let attributes = &self.attributes;
let listeners_names = self
.listeners
.iter()
.map(|(n, _)| syn::parse_str::<Ident>(n).expect(n));
let listeners_exprs = self.listeners.iter().map(|(_, e)| e);
let key = match &self.key {
Some(k) => quote!(Some(#k)),
None => quote!(None),
};
tokens.append_all(quote! {
TemplateContext {
nodes: __cx.bump().alloc([#(#nodes),*]),
text_segments: __cx.bump().alloc([#(&*dioxus::core::exports::bumpalo::format!(in __bump, "{}", #text).into_bump_str()),*]),
attributes: __cx.bump().alloc([#(#attributes),*]),
listeners: __cx.bump().alloc([#(dioxus_elements::on::#listeners_names(__cx, #listeners_exprs)),*]),
key: #key,
}
})
}
}

View file

@ -1,24 +0,0 @@
[package]
name = "dioxus-rsx-interpreter"
version = "0.1.0"
edition = "2021"
license = "MIT/Apache-2.0"
[dependencies]
syn = { version = "1.0", features = ["extra-traits"] }
proc-macro2 = { version = "1.0.39", features = ["span-locations"] }
quote = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
lazy_static = "1.4.0"
dioxus-rsx = { path = "../rsx", default-features = false }
dioxus-ssr = { path = "../ssr" }
dioxus-core = { path = "../core" }
dioxus-html = { path = "../html" }
dioxus-hooks = { path = "../hooks"}
[dev-dependencies]
dioxus-core-macro = { path = "../core-macro" }
bumpalo = { version = "3.6", features = ["collections", "boxed"] }
dioxus = { path = "../dioxus", features = ["hot-reload"]}

View file

@ -1,209 +0,0 @@
use std::collections::HashSet;
use dioxus_core::{Listener, VNode};
use dioxus_rsx::{
BodyNode, CallBody, Component, ElementAttr, ElementAttrNamed, IfmtInput, Segment,
};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{Expr, Ident, LitStr, Result};
use crate::{attributes::attrbute_to_static_str, CodeLocation};
#[derive(Default)]
pub struct CapturedContextBuilder {
pub ifmted: Vec<IfmtInput>,
pub components: Vec<Component>,
pub iterators: Vec<BodyNode>,
pub captured_expressions: Vec<Expr>,
pub listeners: Vec<ElementAttrNamed>,
pub custom_context: Option<Ident>,
pub custom_attributes: HashSet<LitStr>,
}
impl CapturedContextBuilder {
pub fn extend(&mut self, other: CapturedContextBuilder) {
self.ifmted.extend(other.ifmted);
self.components.extend(other.components);
self.iterators.extend(other.iterators);
self.listeners.extend(other.listeners);
self.captured_expressions.extend(other.captured_expressions);
self.custom_attributes.extend(other.custom_attributes);
}
pub fn from_call_body(body: CallBody) -> Result<Self> {
let mut new = Self::default();
for node in body.roots {
new.extend(Self::find_captured(node)?);
}
Ok(new)
}
fn find_captured(node: BodyNode) -> Result<Self> {
let mut captured = CapturedContextBuilder::default();
match node {
BodyNode::Element(el) => {
for attr in el.attributes {
match attr.attr {
ElementAttr::AttrText { value, .. } => {
let value_tokens = value.to_token_stream();
let formated: IfmtInput = syn::parse2(value_tokens)?;
captured.ifmted.push(formated);
}
ElementAttr::CustomAttrText { value, name } => {
captured.custom_attributes.insert(name);
let value_tokens = value.to_token_stream();
let formated: IfmtInput = syn::parse2(value_tokens)?;
captured.ifmted.push(formated);
}
ElementAttr::AttrExpression { name: _, value } => {
captured.captured_expressions.push(value);
}
ElementAttr::CustomAttrExpression { name, value } => {
captured.custom_attributes.insert(name);
captured.captured_expressions.push(value);
}
ElementAttr::EventTokens { .. } => captured.listeners.push(attr),
}
}
if let Some(key) = el.key {
let value_tokens = key.to_token_stream();
let formated: IfmtInput = syn::parse2(value_tokens)?;
captured.ifmted.push(formated);
}
for child in el.children {
captured.extend(Self::find_captured(child)?);
}
}
BodyNode::Component(comp) => {
captured.components.push(comp);
}
BodyNode::Text(t) => {
let tokens = t.to_token_stream();
let formated: IfmtInput = syn::parse2(tokens).unwrap();
captured.ifmted.push(formated);
}
BodyNode::RawExpr(_) => captured.iterators.push(node),
}
Ok(captured)
}
}
impl ToTokens for CapturedContextBuilder {
fn to_tokens(&self, tokens: &mut quote::__private::TokenStream) {
let CapturedContextBuilder {
ifmted,
components,
iterators,
captured_expressions,
listeners,
custom_context: _,
custom_attributes,
} = self;
let listeners_str = listeners
.iter()
.map(|comp| comp.to_token_stream().to_string());
let compontents_str = components
.iter()
.map(|comp| comp.to_token_stream().to_string());
let iterators_str = iterators.iter().map(|node| match node {
BodyNode::RawExpr(expr) => expr.to_token_stream().to_string(),
_ => unreachable!(),
});
let captured: Vec<_> = ifmted
.iter()
.flat_map(|input| input.segments.iter())
.filter_map(|seg| match seg {
Segment::Formatted {
format_args,
segment,
} => {
let expr = segment.to_token_stream();
let as_string = expr.to_string();
let format_expr = if format_args.is_empty() {
"{".to_string() + format_args + "}"
} else {
"{".to_string() + ":" + format_args + "}"
};
Some(quote! {
FormattedArg{
expr: #as_string,
format_args: #format_args,
result: format!(#format_expr, #expr)
}
})
}
_ => None,
})
.collect();
let captured_attr_expressions_text = captured_expressions
.iter()
.map(|e| format!("{}", e.to_token_stream()));
let custom_attributes_iter = custom_attributes.iter();
tokens.append_all(quote! {
CapturedContext {
captured: IfmtArgs{
named_args: vec![#(#captured),*]
},
components: vec![#((#compontents_str, #components)),*],
iterators: vec![#((#iterators_str, #iterators)),*],
expressions: vec![#((#captured_attr_expressions_text, #captured_expressions.to_string())),*],
listeners: vec![#((#listeners_str, #listeners)),*],
custom_attributes: &[#(#custom_attributes_iter),*],
location: code_location.clone()
}
})
}
}
pub struct CapturedContext<'a> {
// map of the variable name to the formated value
pub captured: IfmtArgs,
// map of the attribute name and element path to the formated value
// pub captured_attribute_values: IfmtArgs,
// the only thing we can update in component is the children
pub components: Vec<(&'static str, VNode<'a>)>,
// we can't reasonably interpert iterators, so they are staticly inserted
pub iterators: Vec<(&'static str, VNode<'a>)>,
// map expression to the value resulting from the expression
pub expressions: Vec<(&'static str, String)>,
// map listener code to the resulting listener
pub listeners: Vec<(&'static str, Listener<'a>)>,
// used to map custom attrbutes form &'a str to &'static str
pub custom_attributes: &'static [&'static str],
// used to provide better error messages
pub location: CodeLocation,
}
impl<'a> CapturedContext<'a> {
pub fn attrbute_to_static_str(
&self,
attr: &str,
tag: &'static str,
ns: Option<&'static str>,
literal: bool,
) -> Option<(&'static str, Option<&'static str>)> {
if let Some(attr) = attrbute_to_static_str(attr, tag, ns) {
Some(attr)
} else if literal {
self.custom_attributes
.iter()
.find(|attribute| attr == **attribute)
.map(|attribute| (*attribute, None))
} else {
None
}
}
}
pub struct IfmtArgs {
// All expressions that have been resolved
pub named_args: Vec<FormattedArg>,
}
/// A formated segment that has been resolved
pub struct FormattedArg {
pub expr: &'static str,
pub format_args: &'static str,
pub result: String,
}

View file

@ -1,277 +0,0 @@
use dioxus_core::{Attribute, AttributeValue, NodeFactory, VNode};
use dioxus_rsx::{BodyNode, CallBody, ElementAttr, IfmtInput, Segment};
use quote::ToTokens;
use quote::__private::Span;
use std::str::FromStr;
use syn::{parse2, parse_str, Expr};
use crate::captuered_context::{CapturedContext, IfmtArgs};
use crate::elements::element_to_static_str;
use crate::error::{Error, ParseError, RecompileReason};
fn resolve_ifmt(ifmt: &IfmtInput, captured: &IfmtArgs) -> Result<String, Error> {
let mut result = String::new();
for seg in &ifmt.segments {
match seg {
Segment::Formatted {
segment,
format_args,
} => {
let expr = segment.to_token_stream();
let expr: Expr = parse2(expr).unwrap();
let search = captured.named_args.iter().find(|fmted| {
parse_str::<Expr>(fmted.expr).unwrap() == expr
&& fmted.format_args == format_args
});
match search {
Some(formatted) => {
result.push_str(&formatted.result);
}
None => {
let expr_str = segment.to_token_stream().to_string();
return Err(Error::RecompileRequiredError(
RecompileReason::CapturedExpression(format!(
"could not resolve {{{}:{}}}",
expr_str, format_args
)),
));
}
}
}
Segment::Literal(lit) => result.push_str(lit),
}
}
Ok(result)
}
pub fn build<'a>(
rsx: CallBody,
mut ctx: CapturedContext<'a>,
factory: &NodeFactory<'a>,
) -> Result<VNode<'a>, Error> {
let children_built = factory.bump().alloc(Vec::new());
for child in rsx.roots {
children_built.push(build_node(child, &mut ctx, factory)?);
}
if children_built.len() == 1 {
Ok(children_built.pop().unwrap())
} else {
Ok(factory.fragment_from_iter(children_built.iter()))
}
}
fn build_node<'a>(
node: BodyNode,
ctx: &mut CapturedContext<'a>,
factory: &NodeFactory<'a>,
) -> Result<VNode<'a>, Error> {
let bump = factory.bump();
match node {
BodyNode::Text(text) => {
let ifmt = IfmtInput::from_str(&text.value())
.map_err(|err| Error::ParseError(ParseError::new(err, ctx.location.clone())))?;
let text = bump.alloc(resolve_ifmt(&ifmt, &ctx.captured)?);
Ok(factory.text(format_args!("{}", text)))
}
BodyNode::Element(el) => {
let attributes: &mut Vec<Attribute> = bump.alloc(Vec::new());
let tag = &el.name.to_string();
if let Some((tag, ns)) = element_to_static_str(tag) {
for attr in &el.attributes {
match &attr.attr {
ElementAttr::AttrText { .. } | ElementAttr::CustomAttrText { .. } => {
let (name, value, span, literal): (String, IfmtInput, Span, bool) =
match &attr.attr {
ElementAttr::AttrText { name, value } => (
name.to_string(),
IfmtInput::from_str(&value.value()).map_err(|err| {
Error::ParseError(ParseError::new(
err,
ctx.location.clone(),
))
})?,
name.span(),
false,
),
ElementAttr::CustomAttrText { name, value } => (
name.value(),
IfmtInput::from_str(&value.value()).map_err(|err| {
Error::ParseError(ParseError::new(
err,
ctx.location.clone(),
))
})?,
name.span(),
true,
),
_ => unreachable!(),
};
if let Some((name, namespace)) =
ctx.attrbute_to_static_str(&name, tag, ns, literal)
{
let value = bump.alloc(resolve_ifmt(&value, &ctx.captured)?);
attributes.push(Attribute {
name,
value: AttributeValue::Text(value),
is_static: true,
is_volatile: false,
namespace,
});
} else if literal {
// literals will be captured when a full recompile is triggered
return Err(Error::RecompileRequiredError(
RecompileReason::CapturedAttribute(name.to_string()),
));
} else {
return Err(Error::ParseError(ParseError::new(
syn::Error::new(span, format!("unknown attribute: {}", name)),
ctx.location.clone(),
)));
}
}
ElementAttr::AttrExpression { .. }
| ElementAttr::CustomAttrExpression { .. } => {
let (name, value, span, literal) = match &attr.attr {
ElementAttr::AttrExpression { name, value } => {
(name.to_string(), value, name.span(), false)
}
ElementAttr::CustomAttrExpression { name, value } => {
(name.value(), value, name.span(), true)
}
_ => unreachable!(),
};
if let Some((_, resulting_value)) = ctx
.expressions
.iter()
.find(|(n, _)| parse_str::<Expr>(*n).unwrap() == *value)
{
if let Some((name, namespace)) =
ctx.attrbute_to_static_str(&name, tag, ns, literal)
{
let value = bump.alloc(resulting_value.clone());
attributes.push(Attribute {
name,
value: AttributeValue::Text(value),
is_static: true,
is_volatile: false,
namespace,
});
} else if literal {
// literals will be captured when a full recompile is triggered
return Err(Error::RecompileRequiredError(
RecompileReason::CapturedAttribute(name.to_string()),
));
} else {
return Err(Error::ParseError(ParseError::new(
syn::Error::new(
span,
format!("unknown attribute: {}", name),
),
ctx.location.clone(),
)));
}
}
}
_ => (),
}
}
let children = bump.alloc(Vec::new());
for child in el.children {
let node = build_node(child, ctx, factory)?;
children.push(node);
}
let listeners = bump.alloc(Vec::new());
for attr in el.attributes {
if let ElementAttr::EventTokens { .. } = attr.attr {
let expr: Expr = parse2(attr.to_token_stream()).map_err(|err| {
Error::ParseError(ParseError::new(err, ctx.location.clone()))
})?;
if let Some(idx) = ctx.listeners.iter().position(|(code, _)| {
if let Ok(parsed) = parse_str::<Expr>(*code) {
parsed == expr
} else {
false
}
}) {
let (_, listener) = ctx.listeners.remove(idx);
listeners.push(listener)
} else {
return Err(Error::RecompileRequiredError(
RecompileReason::CapturedListener(
expr.to_token_stream().to_string(),
),
));
}
}
}
match el.key {
None => Ok(factory.raw_element(
tag,
ns,
listeners,
attributes.as_slice(),
children.as_slice(),
None,
)),
Some(lit) => {
let ifmt: IfmtInput = lit.value().parse().map_err(|err| {
Error::ParseError(ParseError::new(err, ctx.location.clone()))
})?;
let key = bump.alloc(resolve_ifmt(&ifmt, &ctx.captured)?);
Ok(factory.raw_element(
tag,
ns,
listeners,
attributes.as_slice(),
children.as_slice(),
Some(format_args!("{}", key)),
))
}
}
} else {
Err(Error::ParseError(ParseError::new(
syn::Error::new(el.name.span(), format!("unknown element: {}", tag)),
ctx.location.clone(),
)))
}
}
BodyNode::Component(comp) => {
let expr: Expr = parse2(comp.to_token_stream())
.map_err(|err| Error::ParseError(ParseError::new(err, ctx.location.clone())))?;
if let Some(idx) = ctx.components.iter().position(|(code, _)| {
if let Ok(parsed) = parse_str::<Expr>(*code) {
parsed == expr
} else {
false
}
}) {
let (_, vnode) = ctx.components.remove(idx);
Ok(vnode)
} else {
Err(Error::RecompileRequiredError(
RecompileReason::CapturedComponent(comp.name.to_token_stream().to_string()),
))
}
}
BodyNode::RawExpr(iterator) => {
if let Some(idx) = ctx.iterators.iter().position(|(code, _)| {
if let Ok(parsed) = parse_str::<Expr>(*code) {
parsed == iterator
} else {
false
}
}) {
let (_, vnode) = ctx.iterators.remove(idx);
Ok(vnode)
} else {
Err(Error::RecompileRequiredError(
RecompileReason::CapturedExpression(iterator.to_token_stream().to_string()),
))
}
}
}
}

View file

@ -1,183 +0,0 @@
use captuered_context::CapturedContext;
use dioxus_core::{NodeFactory, SchedulerMsg, VNode};
use dioxus_hooks::UnboundedSender;
use error::{Error, ParseError};
use interperter::build;
use lazy_static::lazy_static;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{RwLock, RwLockReadGuard};
use syn::parse_str;
mod attributes;
pub mod captuered_context;
mod elements;
pub mod error;
mod interperter;
lazy_static! {
/// This a a global store of the current rsx text for each call to rsx
// Global mutable data is genrally not great, but it allows users to not worry about passing down the text RsxContex every time they switch to hot reloading.
pub static ref RSX_CONTEXT: RsxContext = RsxContext::new(RsxData::default());
}
// the location of the code relative to the current crate based on [std::panic::Location]
#[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeLocation {
pub crate_path: String,
pub file_path: String,
pub line: u32,
pub column: u32,
}
/// Get the resolved rsx given the origional rsx, a captured context of dynamic components, and a factory to build the resulting node
#[cfg_attr(
not(debug_assertions),
deprecated(
note = "The hot reload feature is enabled in release mode. This feature should be disabled for production builds."
)
)]
pub fn resolve_scope<'a>(
location: CodeLocation,
rsx: &'static str,
captured: CapturedContext<'a>,
factory: NodeFactory<'a>,
) -> VNode<'a> {
let rsx_text_index = &*RSX_CONTEXT;
// only the insert the rsx text once
if !rsx_text_index.read().hm.contains_key(&location) {
rsx_text_index.insert(location.clone(), rsx.to_string());
}
if let Some(text) = {
let read = rsx_text_index.read();
// clone prevents deadlock on nested rsx calls
read.hm.get(&location).cloned()
} {
match interpert_rsx(factory, &text, captured) {
Ok(vnode) => vnode,
Err(err) => {
rsx_text_index.report_error(err);
factory.text(format_args!(""))
}
}
} else {
panic!("rsx: line number {:?} not found in rsx index", location);
}
}
fn interpert_rsx<'a>(
factory: dioxus_core::NodeFactory<'a>,
text: &str,
context: captuered_context::CapturedContext<'a>,
) -> Result<VNode<'a>, Error> {
build(
parse_str(text)
.map_err(|err| Error::ParseError(ParseError::new(err, context.location.clone())))?,
context,
&factory,
)
}
/// get the code location of the code that called this function
#[macro_export]
macro_rules! get_line_num {
() => {{
let line = line!();
let column = column!();
let file_path = file!().to_string();
let crate_path = env!("CARGO_MANIFEST_DIR").to_string();
CodeLocation {
crate_path,
file_path,
line: line,
column: column,
}
}};
}
/// A handle to the rsx context with interior mutability
#[derive(Debug)]
pub struct RsxContext {
data: RwLock<RsxData>,
}
/// A store of the text for the rsx macro for each call to rsx
#[derive(Default)]
pub struct RsxData {
pub hm: HashMap<CodeLocation, String>,
pub error_handler: Option<Box<dyn ErrorHandler>>,
pub scheduler_channel: Option<UnboundedSender<SchedulerMsg>>,
}
impl std::fmt::Debug for RsxData {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RsxData").field("hm", &self.hm).finish()
}
}
impl RsxContext {
pub fn new(data: RsxData) -> Self {
Self {
data: RwLock::new(data),
}
}
/// Set the text for an rsx call at some location
pub fn insert(&self, loc: CodeLocation, text: String) {
let mut write = self.data.write().unwrap();
write.hm.insert(loc, text);
if let Some(channel) = &mut write.scheduler_channel {
channel.unbounded_send(SchedulerMsg::DirtyAll).unwrap()
}
}
/// Set the text for many rsx calls
pub fn extend(&self, msg: SetManyRsxMessage) {
let mut write = self.data.write().unwrap();
for rsx in msg.0 {
write.hm.insert(rsx.location, rsx.new_text);
}
if let Some(channel) = &mut write.scheduler_channel {
channel.unbounded_send(SchedulerMsg::DirtyAll).unwrap()
}
}
fn read(&self) -> RwLockReadGuard<RsxData> {
self.data.read().unwrap()
}
fn report_error(&self, error: Error) {
if let Some(handler) = &self.data.write().unwrap().error_handler {
handler.handle_error(error)
} else {
panic!("no error handler set for this platform...\n{}", error);
}
}
/// Set the handler for errors interperting the rsx
pub fn set_error_handler(&self, handler: impl ErrorHandler + 'static) {
self.data.write().unwrap().error_handler = Some(Box::new(handler));
}
/// Provide the scduler channel from [dioxus_code::VirtualDom::get_scheduler_channel].
/// The channel allows the interpreter to force re-rendering of the dom when the rsx is changed.
pub fn provide_scheduler_channel(&self, channel: UnboundedSender<SchedulerMsg>) {
self.data.write().unwrap().scheduler_channel = Some(channel)
}
}
/// A error handler for errors reported by the rsx interperter
pub trait ErrorHandler: Send + Sync {
fn handle_error(&self, err: Error);
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SetRsxMessage {
pub location: CodeLocation,
pub new_text: String,
}
/// Set many rsx texts at once to avoid duplicate errors
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct SetManyRsxMessage(pub Vec<SetRsxMessage>);

View file

@ -1,407 +0,0 @@
use dioxus::prelude::*;
#[test]
#[allow(non_snake_case)]
fn render_basic() {
fn Base(cx: Scope) -> Element {
render!(div {})
}
let dom = VirtualDom::new(Base);
let static_vnodes = rsx!(div{"hello world"});
let location = CodeLocation {
file_path: String::new(),
crate_path: String::new(),
line: 0,
column: 0,
};
let empty_context = CapturedContext {
captured: IfmtArgs {
named_args: Vec::new(),
},
components: Vec::new(),
iterators: Vec::new(),
expressions: Vec::new(),
listeners: Vec::new(),
location: location.clone(),
custom_attributes: &[],
};
let interperted_vnodes = LazyNodes::new(|factory| {
dioxus_rsx_interpreter::resolve_scope(
location,
"div{\"hello world\"}",
empty_context,
factory,
)
});
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
let static_vnodes = dom.render_vnodes(static_vnodes);
assert!(check_eq(interperted_vnodes, static_vnodes));
}
#[test]
#[allow(non_snake_case)]
fn render_nested() {
fn Base(cx: Scope) -> Element {
render!(div {})
}
let dom = VirtualDom::new(Base);
let static_vnodes = rsx! {
div {
p { "hello world" }
div {
p { "hello world" }
}
}
};
let location = CodeLocation {
file_path: String::new(),
crate_path: String::new(),
line: 1,
column: 0,
};
let empty_context = CapturedContext {
captured: IfmtArgs {
named_args: Vec::new(),
},
components: Vec::new(),
iterators: Vec::new(),
expressions: Vec::new(),
listeners: Vec::new(),
location: location.clone(),
custom_attributes: &[],
};
let interperted_vnodes = LazyNodes::new(|factory| {
dioxus_rsx_interpreter::resolve_scope(
location,
r#"div {
p { "hello world" }
div {
p { "hello world" }
}
}"#,
empty_context,
factory,
)
});
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
let static_vnodes = dom.render_vnodes(static_vnodes);
assert!(check_eq(interperted_vnodes, static_vnodes));
}
#[test]
#[allow(non_snake_case)]
fn render_custom_attribute() {
fn Base(cx: Scope) -> Element {
render!(div {})
}
let dom = VirtualDom::new(Base);
let static_vnodes = rsx! {
div {
"data-test-1": 0,
"data-test-2": "1",
}
};
let location = CodeLocation {
file_path: String::new(),
crate_path: String::new(),
line: 2,
column: 0,
};
let empty_context = CapturedContext {
captured: IfmtArgs {
named_args: Vec::new(),
},
components: Vec::new(),
iterators: Vec::new(),
expressions: vec![("0", "0".to_string())],
listeners: Vec::new(),
location: location.clone(),
custom_attributes: &["data-test-1", "data-test-2"],
};
let interperted_vnodes = LazyNodes::new(|factory| {
dioxus_rsx_interpreter::resolve_scope(
location,
r#"div {
"data-test-1": 0,
"data-test-2": "1",
}"#,
empty_context,
factory,
)
});
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
let static_vnodes = dom.render_vnodes(static_vnodes);
assert!(check_eq(interperted_vnodes, static_vnodes));
}
#[test]
#[allow(non_snake_case)]
fn render_component() {
fn Comp(cx: Scope) -> Element {
render!(div {})
}
fn Base(cx: Scope) -> Element {
render!(div {})
}
let dom = VirtualDom::new(Base);
let static_vnodes = rsx! {
div {
Comp {}
}
};
let location = CodeLocation {
file_path: String::new(),
crate_path: String::new(),
line: 3,
column: 0,
};
let interperted_vnodes = LazyNodes::new(|factory| {
let context = CapturedContext {
captured: IfmtArgs {
named_args: Vec::new(),
},
components: vec![(
r#"__cx.component(Comp, fc_to_builder(Comp).build(), None, "Comp")"#,
factory.component(Comp, (), None, "Comp"),
)],
iterators: Vec::new(),
expressions: Vec::new(),
listeners: Vec::new(),
location: location.clone(),
custom_attributes: &[],
};
dioxus_rsx_interpreter::resolve_scope(
location,
r#"div {
Comp {}
}"#,
context,
factory,
)
});
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
let static_vnodes = dom.render_vnodes(static_vnodes);
println!("{:#?}", interperted_vnodes);
println!("{:#?}", static_vnodes);
assert!(check_eq(interperted_vnodes, static_vnodes));
}
#[test]
#[allow(non_snake_case)]
fn render_iterator() {
fn Base(cx: Scope) -> Element {
render!(div {})
}
let dom = VirtualDom::new(Base);
let iter = (0..10).map(|i| dom.render_vnodes(rsx! {"{i}"}));
let static_vnodes = rsx! {
div {
iter
}
};
let location = CodeLocation {
file_path: String::new(),
crate_path: String::new(),
line: 4,
column: 0,
};
let interperted_vnodes = LazyNodes::new(|factory| {
let context = CapturedContext {
captured: IfmtArgs {
named_args: Vec::new(),
},
components: Vec::new(),
iterators: vec![(
r#"
(0..10).map(|i| dom.render_vnodes(rsx!{"{i}"}))"#,
factory.fragment_from_iter((0..10).map(|i| factory.text(format_args!("{i}")))),
)],
expressions: Vec::new(),
listeners: Vec::new(),
location: location.clone(),
custom_attributes: &[],
};
dioxus_rsx_interpreter::resolve_scope(
location,
r#"div {
(0..10).map(|i| dom.render_vnodes(rsx!{"{i}"}))
}"#,
context,
factory,
)
});
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
let static_vnodes = dom.render_vnodes(static_vnodes);
println!("{:#?}", interperted_vnodes);
println!("{:#?}", static_vnodes);
assert!(check_eq(interperted_vnodes, static_vnodes));
}
#[test]
#[allow(non_snake_case)]
fn render_captured_variable() {
fn Base(cx: Scope) -> Element {
render!(div {})
}
let dom = VirtualDom::new(Base);
let x = 10;
let static_vnodes = rsx! {
div {
"{x}"
}
};
let location = CodeLocation {
file_path: String::new(),
crate_path: String::new(),
line: 5,
column: 0,
};
let interperted_vnodes = LazyNodes::new(|factory| {
let context = CapturedContext {
captured: IfmtArgs {
named_args: vec![FormattedArg {
expr: "x",
format_args: "",
result: x.to_string(),
}],
},
components: Vec::new(),
iterators: Vec::new(),
expressions: Vec::new(),
listeners: Vec::new(),
location: location.clone(),
custom_attributes: &[],
};
dioxus_rsx_interpreter::resolve_scope(
location,
r#"div {
"{x}"
}"#,
context,
factory,
)
});
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
let static_vnodes = dom.render_vnodes(static_vnodes);
println!("{:#?}", interperted_vnodes);
println!("{:#?}", static_vnodes);
assert!(check_eq(interperted_vnodes, static_vnodes));
}
#[test]
#[allow(non_snake_case)]
fn render_listener() {
fn Base(cx: Scope) -> Element {
render!(div {})
}
let dom = VirtualDom::new(Base);
let static_vnodes = rsx! {
div {
onclick: |_| println!("clicked")
}
};
let location = CodeLocation {
file_path: String::new(),
crate_path: String::new(),
line: 6,
column: 0,
};
let interperted_vnodes = LazyNodes::new(|factory| {
let f = |_| println!("clicked");
let f = factory.bump().alloc(f);
let context = CapturedContext {
captured: IfmtArgs {
named_args: Vec::new(),
},
components: Vec::new(),
iterators: Vec::new(),
expressions: Vec::new(),
listeners: vec![(
r#"dioxus_elements::on::onclick(__cx, |_| println!("clicked"))"#,
dioxus_elements::on::onclick(factory, f),
)],
location: location.clone(),
custom_attributes: &[],
};
dioxus_rsx_interpreter::resolve_scope(
location,
r#"div {
onclick: |_| println!("clicked")
}"#,
context,
factory,
)
});
let interperted_vnodes = dom.render_vnodes(interperted_vnodes);
let static_vnodes = dom.render_vnodes(static_vnodes);
println!("{:#?}", interperted_vnodes);
println!("{:#?}", static_vnodes);
assert!(check_eq(interperted_vnodes, static_vnodes));
}
fn check_eq<'a>(a: &'a VNode<'a>, b: &'a VNode<'a>) -> bool {
match (a, b) {
(VNode::Text(t_a), VNode::Text(t_b)) => t_a.text == t_b.text,
(VNode::Element(e_a), VNode::Element(e_b)) => {
e_a.attributes
.iter()
.zip(e_b.attributes.iter())
.all(|(a, b)| {
a.is_static == b.is_static
&& a.is_volatile == b.is_volatile
&& a.name == b.name
&& a.value == b.value
&& a.namespace == b.namespace
})
&& e_a
.children
.iter()
.zip(e_b.children.iter())
.all(|(a, b)| check_eq(a, b))
&& e_a.key == e_b.key
&& e_a.tag == e_b.tag
&& e_a.namespace == e_b.namespace
&& e_a
.listeners
.iter()
.zip(e_b.listeners.iter())
.all(|(a, b)| a.event == b.event)
}
(VNode::Fragment(f_a), VNode::Fragment(f_b)) => {
f_a.key == f_b.key
&& f_a
.children
.iter()
.zip(f_b.children.iter())
.all(|(a, b)| check_eq(a, b))
}
(VNode::Component(c_a), VNode::Component(c_b)) => {
c_a.can_memoize == c_b.can_memoize
&& c_a.key == c_b.key
&& c_a.fn_name == c_b.fn_name
&& c_a.user_fc == c_b.user_fc
}
(VNode::Placeholder(_), VNode::Placeholder(_)) => true,
_ => false,
}
}

View file

@ -1,7 +1,8 @@
#![doc = include_str!("../README.md")]
use std::fmt::{Display, Formatter};
use std::fmt::{Display, Formatter, Write};
use dioxus_core::exports::bumpalo;
use dioxus_core::IntoVNode;
use dioxus_core::*;
@ -32,7 +33,8 @@ impl SsrRenderer {
TextRenderer {
cfg: self.cfg.clone(),
root: &root,
vdom: None
vdom: Some(&self.vdom),
bump: bumpalo::Bump::new(),
}
)
}
@ -57,14 +59,18 @@ pub fn render_lazy<'a>(f: LazyNodes<'a, '_>) -> String {
let root = f.into_vnode(NodeFactory::new(scope));
format!(
"{:}",
TextRenderer {
cfg: SsrConfig::default(),
root: &root,
vdom: None
}
)
let vdom = Some(&vdom);
let ssr_renderer = TextRenderer {
cfg: SsrConfig::default(),
root: &root,
vdom,
bump: bumpalo::Bump::new(),
};
let r = ssr_renderer.to_string();
drop(ssr_renderer);
drop(vdom);
r
}
pub fn render_vdom(dom: &VirtualDom) -> String {
@ -91,7 +97,8 @@ pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> {
TextRenderer {
cfg: SsrConfig::default(),
root: vdom.get_scope(scope).unwrap().root_node(),
vdom: Some(vdom)
vdom: Some(vdom),
bump: bumpalo::Bump::new()
}
))
}
@ -114,32 +121,36 @@ pub fn render_vdom_scope(vdom: &VirtualDom, scope: ScopeId) -> Option<String> {
/// let output = format!("{}", renderer);
/// assert_eq!(output, "<div>hello world</div>");
/// ```
pub struct TextRenderer<'a, 'b> {
vdom: Option<&'a VirtualDom>,
pub struct TextRenderer<'a, 'b, 'c> {
vdom: Option<&'c VirtualDom>,
root: &'b VNode<'a>,
cfg: SsrConfig,
bump: bumpalo::Bump,
}
impl Display for TextRenderer<'_, '_> {
impl<'a: 'c, 'c> Display for TextRenderer<'a, '_, 'c> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut last_node_was_text = false;
self.html_render(self.root, f, 0, &mut last_node_was_text)
}
}
impl<'a> TextRenderer<'a, '_> {
impl<'a> TextRenderer<'a, '_, 'a> {
pub fn from_vdom(vdom: &'a VirtualDom, cfg: SsrConfig) -> Self {
Self {
cfg,
root: vdom.base_scope().root_node(),
vdom: Some(vdom),
bump: bumpalo::Bump::new(),
}
}
}
impl<'a: 'c, 'c> TextRenderer<'a, '_, 'c> {
fn html_render(
&self,
node: &VNode,
f: &mut std::fmt::Formatter,
f: &mut impl Write,
il: u16,
last_node_was_text: &mut bool,
) -> std::fmt::Result {
@ -180,66 +191,7 @@ impl<'a> TextRenderer<'a, '_> {
write!(f, "<{}", el.tag)?;
let mut inner_html = None;
let mut attr_iter = el.attributes.iter().peekable();
while let Some(attr) = attr_iter.next() {
match attr.namespace {
None => match attr.name {
"dangerous_inner_html" => {
inner_html = Some(attr.value.as_text().unwrap())
}
"allowfullscreen"
| "allowpaymentrequest"
| "async"
| "autofocus"
| "autoplay"
| "checked"
| "controls"
| "default"
| "defer"
| "disabled"
| "formnovalidate"
| "hidden"
| "ismap"
| "itemscope"
| "loop"
| "multiple"
| "muted"
| "nomodule"
| "novalidate"
| "open"
| "playsinline"
| "readonly"
| "required"
| "reversed"
| "selected"
| "truespeed" => {
if attr.value.is_truthy() {
write!(f, " {}=\"{}\"", attr.name, attr.value)?
}
}
_ => write!(f, " {}=\"{}\"", attr.name, attr.value)?,
},
Some(ns) => {
// write the opening tag
write!(f, " {}=\"", ns)?;
let mut cur_ns_el = attr;
'ns_parse: loop {
write!(f, "{}:{};", cur_ns_el.name, cur_ns_el.value)?;
match attr_iter.peek() {
Some(next_attr) if next_attr.namespace == Some(ns) => {
cur_ns_el = attr_iter.next().unwrap();
}
_ => break 'ns_parse,
}
}
// write the closing tag
write!(f, "\"")?;
}
}
}
let inner_html = render_attributes(el.attributes.iter(), f)?;
match self.cfg.newline {
true => writeln!(f, ">")?,
@ -283,9 +235,329 @@ impl<'a> TextRenderer<'a, '_> {
} else {
}
}
VNode::TemplateRef(tmpl) => {
if let Some(vdom) = self.vdom {
let template_id = &tmpl.template_id;
let dynamic_context = &tmpl.dynamic_context;
vdom.with_template(template_id, move |tmpl| {
match tmpl {
Template::Static(s) => {
for r in s.root_nodes {
self.render_template_node(
&s.nodes,
&s.nodes[r.0],
dynamic_context,
&s.dynamic_mapping,
f,
last_node_was_text,
il,
)?;
}
}
Template::Owned(o) => {
for r in &o.root_nodes {
self.render_template_node(
&o.nodes,
&o.nodes[r.0],
dynamic_context,
&o.dynamic_mapping,
f,
last_node_was_text,
il,
)?;
}
}
};
Ok(())
})?
} else {
panic!("Cannot render template without vdom");
}
}
}
Ok(())
}
fn render_template_node<
TemplateNodes,
Attributes,
V,
Children,
Listeners,
TextSegments,
Text,
Nodes,
TextOuter,
TextInner,
AttributesOuter,
AttributesInner,
Volatile,
Listeners2,
>(
&self,
template_nodes: &TemplateNodes,
node: &TemplateNode<Attributes, V, Children, Listeners, TextSegments, Text>,
dynamic_context: &TemplateContext,
dynamic_node_mapping: &DynamicNodeMapping<
Nodes,
TextOuter,
TextInner,
AttributesOuter,
AttributesInner,
Volatile,
Listeners2,
>,
f: &mut impl Write,
last_node_was_text: &mut bool,
il: u16,
) -> std::fmt::Result
where
TemplateNodes:
AsRef<[TemplateNode<Attributes, V, Children, Listeners, TextSegments, Text>]>,
Attributes: AsRef<[TemplateAttribute<V>]>,
AttributesInner: AsRef<[(TemplateNodeId, usize)]>,
AttributesOuter: AsRef<[AttributesInner]>,
Children: AsRef<[TemplateNodeId]>,
Listeners: AsRef<[usize]>,
Listeners2: AsRef<[TemplateNodeId]>,
Nodes: AsRef<[Option<TemplateNodeId>]>,
Text: AsRef<str>,
TextInner: AsRef<[TemplateNodeId]>,
TextOuter: AsRef<[TextInner]>,
TextSegments: AsRef<[TextTemplateSegment<Text>]>,
V: TemplateValue,
Volatile: AsRef<[(TemplateNodeId, usize)]>,
{
match &node.node_type {
TemplateNodeType::Element(el) => {
*last_node_was_text = false;
if self.cfg.indent {
for _ in 0..il {
write!(f, " ")?;
}
}
write!(f, "<{}", el.tag)?;
let mut inner_html = None;
let mut attr_iter = el.attributes.as_ref().into_iter().peekable();
while let Some(attr) = attr_iter.next() {
match attr.attribute.namespace {
None => {
if attr.attribute.name == "dangerous_inner_html" {
inner_html = {
let text = match &attr.value {
TemplateAttributeValue::Static(val) => {
val.allocate(&self.bump).as_text().unwrap()
}
TemplateAttributeValue::Dynamic(idx) => dynamic_context
.resolve_attribute(*idx)
.as_text()
.unwrap(),
};
Some(text)
}
} else if is_boolean_attribute(attr.attribute.name) {
match &attr.value {
TemplateAttributeValue::Static(val) => {
let val = val.allocate(&self.bump);
if val.is_truthy() {
write!(f, " {}=\"{}\"", attr.attribute.name, val)?
}
}
TemplateAttributeValue::Dynamic(idx) => {
let val = dynamic_context.resolve_attribute(*idx);
if val.is_truthy() {
write!(f, " {}=\"{}\"", attr.attribute.name, val)?
}
}
}
} else {
match &attr.value {
TemplateAttributeValue::Static(val) => {
let val = val.allocate(&self.bump);
write!(f, " {}=\"{}\"", attr.attribute.name, val)?
}
TemplateAttributeValue::Dynamic(idx) => {
let val = dynamic_context.resolve_attribute(*idx);
write!(f, " {}=\"{}\"", attr.attribute.name, val)?
}
}
}
}
Some(ns) => {
// write the opening tag
write!(f, " {}=\"", ns)?;
let mut cur_ns_el = attr;
loop {
match &attr.value {
TemplateAttributeValue::Static(val) => {
let val = val.allocate(&self.bump);
write!(f, "{}:{};", cur_ns_el.attribute.name, val)?;
}
TemplateAttributeValue::Dynamic(idx) => {
let val = dynamic_context.resolve_attribute(*idx);
write!(f, "{}:{};", cur_ns_el.attribute.name, val)?;
}
}
match attr_iter.peek() {
Some(next_attr)
if next_attr.attribute.namespace == Some(ns) =>
{
cur_ns_el = attr_iter.next().unwrap();
}
_ => break,
}
}
// write the closing tag
write!(f, "\"")?;
}
}
}
match self.cfg.newline {
true => writeln!(f, ">")?,
false => write!(f, ">")?,
}
if let Some(inner_html) = inner_html {
write!(f, "{}", inner_html)?;
} else {
let mut last_node_was_text = false;
for child in el.children.as_ref() {
self.render_template_node(
template_nodes,
&template_nodes.as_ref()[child.0],
dynamic_context,
dynamic_node_mapping,
f,
&mut last_node_was_text,
il + 1,
)?;
}
}
if self.cfg.newline {
writeln!(f)?;
}
if self.cfg.indent {
for _ in 0..il {
write!(f, " ")?;
}
}
write!(f, "</{}>", el.tag)?;
if self.cfg.newline {
writeln!(f)?;
}
}
TemplateNodeType::Text(txt) => {
if *last_node_was_text {
write!(f, "<!--spacer-->")?;
}
if self.cfg.indent {
for _ in 0..il {
write!(f, " ")?;
}
}
*last_node_was_text = true;
let text = dynamic_context.resolve_text(&txt.segments);
write!(f, "{}", text)?
}
TemplateNodeType::DynamicNode(idx) => {
let node = dynamic_context.resolve_node(*idx);
self.html_render(node, f, il, last_node_was_text)?;
}
}
Ok(())
}
}
fn render_attributes<'a, 'b: 'a, I>(
attrs: I,
f: &mut impl Write,
) -> Result<Option<&'b str>, std::fmt::Error>
where
I: Iterator<Item = &'a Attribute<'b>>,
{
let mut inner_html = None;
let mut attr_iter = attrs.peekable();
while let Some(attr) = attr_iter.next() {
match attr.attribute.namespace {
None => {
if attr.attribute.name == "dangerous_inner_html" {
inner_html = Some(attr.value.as_text().unwrap())
} else {
if is_boolean_attribute(attr.attribute.name) {
if !attr.value.is_truthy() {
continue;
}
}
write!(f, " {}=\"{}\"", attr.attribute.name, attr.value)?
}
}
Some(ns) => {
// write the opening tag
write!(f, " {}=\"", ns)?;
let mut cur_ns_el = attr;
loop {
write!(f, "{}:{};", cur_ns_el.attribute.name, cur_ns_el.value)?;
match attr_iter.peek() {
Some(next_attr) if next_attr.attribute.namespace == Some(ns) => {
cur_ns_el = attr_iter.next().unwrap();
}
_ => break,
}
}
// write the closing tag
write!(f, "\"")?;
}
}
}
Ok(inner_html)
}
fn is_boolean_attribute(attribute: &'static str) -> bool {
if let "allowfullscreen"
| "allowpaymentrequest"
| "async"
| "autofocus"
| "autoplay"
| "checked"
| "controls"
| "default"
| "defer"
| "disabled"
| "formnovalidate"
| "hidden"
| "ismap"
| "itemscope"
| "loop"
| "multiple"
| "muted"
| "nomodule"
| "novalidate"
| "open"
| "playsinline"
| "readonly"
| "required"
| "reversed"
| "selected"
| "truespeed" = attribute
{
true
} else {
false
}
}
#[derive(Clone, Debug, Default)]

View file

@ -1,6 +1,6 @@
use crate::{node::PreventDefault, Dom};
use dioxus_core::ElementId;
use dioxus_core::GlobalNodeId;
use dioxus_native_core::utils::{ElementProduced, PersistantElementIter};
use dioxus_native_core_macro::sorted_str_slice;
@ -69,7 +69,10 @@ impl NodeDepState<()> for Focus {
fn reduce(&mut self, node: NodeView<'_>, _sibling: (), _: &Self::Ctx) -> bool {
let new = Focus {
level: if let Some(a) = node.attributes().find(|a| a.name == "tabindex") {
level: if let Some(a) = node
.attributes()
.and_then(|mut a| a.find(|a| a.attribute.name == "tabindex"))
{
if let Some(index) = a
.value
.as_int32()
@ -87,8 +90,12 @@ impl NodeDepState<()> for Focus {
}
} else if node
.listeners()
.iter()
.any(|l| FOCUS_EVENTS.binary_search(&l.event).is_ok())
.map(|mut listeners| {
listeners
.any(|l| FOCUS_EVENTS.binary_search(&l).is_ok())
.then(|| ())
})
.is_some()
{
FocusLevel::Focusable
} else {
@ -110,7 +117,7 @@ const FOCUS_ATTRIBUTES: &[&str] = &sorted_str_slice!(["tabindex"]);
#[derive(Default)]
pub(crate) struct FocusState {
pub(crate) focus_iter: PersistantElementIter,
pub(crate) last_focused_id: Option<ElementId>,
pub(crate) last_focused_id: Option<GlobalNodeId>,
pub(crate) focus_level: FocusLevel,
pub(crate) dirty: bool,
}
@ -224,9 +231,9 @@ impl FocusState {
pub(crate) fn prune(&mut self, mutations: &dioxus_core::Mutations, rdom: &Dom) {
fn remove_children(
to_prune: &mut [&mut Option<ElementId>],
to_prune: &mut [&mut Option<GlobalNodeId>],
rdom: &Dom,
removed: ElementId,
removed: GlobalNodeId,
) {
for opt in to_prune.iter_mut() {
if let Some(id) = opt {
@ -235,7 +242,7 @@ impl FocusState {
}
}
}
if let NodeType::Element { children, .. } = &rdom[removed].node_type {
if let NodeType::Element { children, .. } = &rdom[removed].node_data.node_type {
for child in children {
remove_children(to_prune, rdom, *child);
}
@ -249,19 +256,19 @@ impl FocusState {
dioxus_core::DomEdit::ReplaceWith { root, .. } => remove_children(
&mut [&mut self.last_focused_id],
rdom,
ElementId(*root as usize),
rdom.decode_id(*root),
),
dioxus_core::DomEdit::Remove { root } => remove_children(
&mut [&mut self.last_focused_id],
rdom,
ElementId(*root as usize),
rdom.decode_id(*root),
),
_ => (),
}
}
}
pub(crate) fn set_focus(&mut self, rdom: &mut Dom, id: ElementId) {
pub(crate) fn set_focus(&mut self, rdom: &mut Dom, id: GlobalNodeId) {
if let Some(old) = self.last_focused_id.replace(id) {
rdom[old].state.focused = false;
}

View file

@ -243,23 +243,24 @@ impl InnerInputState {
fn try_create_event(
name: &'static str,
data: Arc<dyn Any + Send + Sync>,
will_bubble: &mut FxHashSet<ElementId>,
will_bubble: &mut FxHashSet<GlobalNodeId>,
resolved_events: &mut Vec<UserEvent>,
node: &Node,
dom: &Dom,
) {
// only trigger event if the event was not triggered already by a child
if will_bubble.insert(node.id) {
let mut parent = node.parent;
let id = node.node_data.id;
if will_bubble.insert(id) {
let mut parent = node.node_data.parent;
while let Some(parent_id) = parent {
will_bubble.insert(parent_id);
parent = dom[parent_id].parent;
parent = dom[parent_id].node_data.parent;
}
resolved_events.push(UserEvent {
scope_id: None,
priority: EventPriority::Medium,
name,
element: Some(node.id),
element: Some(id),
data,
bubbles: event_bubbles(name),
})
@ -546,7 +547,7 @@ impl InnerInputState {
let currently_contains = layout_contains_point(node_layout, new_pos);
if currently_contains && node.state.focus.level.focusable() {
focus_id = Some(node.id);
focus_id = Some(node.node_data.id);
}
});
if let Some(id) = focus_id {
@ -565,7 +566,7 @@ fn get_abs_layout(node: &Node, dom: &Dom, taffy: &Taffy) -> Layout {
let mut node_layout = *taffy.layout(node.state.layout.node.unwrap()).unwrap();
let mut current = node;
while let Some(parent_id) = current.parent {
while let Some(parent_id) = current.node_data.parent {
let parent = &dom[parent_id];
current = parent;
let parent_layout = taffy.layout(parent.state.layout.node.unwrap()).unwrap();
@ -664,7 +665,7 @@ impl RinkInputHandler {
scope_id: None,
priority: EventPriority::Medium,
name: event,
element: Some(node.id),
element: Some(node.node_data.id),
data: data.clone(),
bubbles: event_bubbles(event),
});

View file

@ -4,6 +4,7 @@ use std::rc::Rc;
use dioxus_core::*;
use dioxus_native_core::layout_attributes::apply_layout_attributes;
use dioxus_native_core::node_ref::{AttributeMask, NodeMask, NodeView};
use dioxus_native_core::real_dom::OwnedAttributeView;
use dioxus_native_core::state::ChildDepState;
use dioxus_native_core_macro::sorted_str_slice;
use taffy::prelude::*;
@ -84,10 +85,17 @@ impl ChildDepState for TaffyLayout {
}
} else {
// gather up all the styles from the attribute list
for Attribute { name, value, .. } in node.attributes() {
assert!(SORTED_LAYOUT_ATTRS.binary_search(name).is_ok());
if let Some(text) = value.as_text() {
apply_layout_attributes(name, text, &mut style);
if let Some(attributes) = node.attributes() {
for OwnedAttributeView {
attribute, value, ..
} in attributes
{
assert!(SORTED_LAYOUT_ATTRS
.binary_search(&attribute.name.as_ref())
.is_ok());
if let Some(text) = value.as_text() {
apply_layout_attributes(&&attribute.name, text, &mut style);
}
}
}

View file

@ -94,7 +94,7 @@ pub fn launch_cfg(app: Component<()>, cfg: Config) {
let to_update = rdom.apply_mutations(vec![mutations]);
let mut any_map = AnyMap::new();
any_map.insert(taffy.clone());
let _to_rerender = rdom.update_state(&dom, to_update, any_map);
let _to_rerender = rdom.update_state(to_update, any_map);
}
render_vdom(
@ -133,8 +133,10 @@ fn render_vdom(
terminal.clear().unwrap();
}
let mut to_rerender: fxhash::FxHashSet<ElementId> =
vec![ElementId(0)].into_iter().collect();
let mut to_rerender: fxhash::FxHashSet<GlobalNodeId> =
vec![GlobalNodeId::VNodeId(ElementId(0))]
.into_iter()
.collect();
let mut updated = true;
loop {
@ -250,7 +252,7 @@ fn render_vdom(
// update the style and layout
let mut any_map = AnyMap::new();
any_map.insert(taffy.clone());
to_rerender = rdom.update_state(vdom, to_update, any_map);
to_rerender = rdom.update_state(to_update, any_map);
}
}

View file

@ -61,11 +61,11 @@ impl NodeDepState<()> for PreventDefault {
_sibling: (),
_ctx: &Self::Ctx,
) -> bool {
let new = match node
.attributes()
.find(|a| a.name == "dioxus-prevent-default")
.and_then(|a| a.value.as_text())
{
let new = match node.attributes().and_then(|mut attrs| {
attrs
.find(|a| a.attribute.name == "dioxus-prevent-default")
.and_then(|a| a.value.as_text())
}) {
Some("onfocus") => PreventDefault::Focus,
Some("onkeypress") => PreventDefault::KeyPress,
Some("onkeyrelease") => PreventDefault::KeyRelease,

View file

@ -25,7 +25,7 @@ pub(crate) fn render_vnode(
) {
use dioxus_native_core::real_dom::NodeType;
if let NodeType::Placeholder = &node.node_type {
if let NodeType::Placeholder = &node.node_data.node_type {
return;
}
@ -38,7 +38,7 @@ pub(crate) fn render_vnode(
let Point { x, y } = location;
let Size { width, height } = size;
match &node.node_type {
match &node.node_data.node_type {
NodeType::Text { text } => {
#[derive(Default)]
struct Label<'a> {

View file

@ -29,10 +29,10 @@
- [ ] pub aspect_ratio: Number,
*/
use dioxus_core::Attribute;
use dioxus_native_core::{
layout_attributes::parse_value,
node_ref::{AttributeMask, NodeMask, NodeView},
real_dom::OwnedAttributeView,
state::ParentDepState,
};
use dioxus_native_core_macro::sorted_str_slice;
@ -79,9 +79,14 @@ impl ParentDepState for StyleModifier {
}
// gather up all the styles from the attribute list
for Attribute { name, value, .. } in node.attributes() {
if let Some(text) = value.as_text() {
apply_style_attributes(name, text, &mut new);
if let Some(attrs) = node.attributes() {
for OwnedAttributeView {
attribute, value, ..
} in attrs
{
if let Some(text) = value.as_text() {
apply_style_attributes(&attribute.name, text, &mut new);
}
}
}

View file

@ -16,7 +16,6 @@ dioxus-html = { path = "../html", version = "^0.2.1", features = ["wasm-bind"] }
dioxus-interpreter-js = { path = "../interpreter", version = "^0.2.1", features = [
"web"
] }
dioxus-rsx-interpreter = { path = "../rsx_interpreter", version = "^0.1.0", optional = true }
js-sys = "0.3.56"
wasm-bindgen = { version = "0.2.79", features = ["enable-interning"] }
@ -31,7 +30,7 @@ futures-util = "0.3.19"
smallstr = "0.2.0"
serde-wasm-bindgen = "0.4.2"
futures-channel = "0.3.21"
serde_json = { version = "1.0", optional = true }
serde_json = { version = "1.0" }
[dependencies.web-sys]
version = "0.3.56"
@ -72,12 +71,17 @@ features = [
"SvgAnimatedString",
"HtmlOptionElement",
"IdleDeadline",
"WebSocket",
"Location",
"MessageEvent",
"console",
]
[features]
default = ["panic_hook"]
panic_hook = ["console_error_panic_hook"]
hot-reload = ["dioxus-rsx-interpreter", "web-sys/WebSocket", "web-sys/Location", "web-sys/MessageEvent", "web-sys/console", "serde_json"]
hot-reload = ["dioxus/hot-reload"]
[dev-dependencies]
dioxus = { path = "../dioxus" }

View file

@ -24,6 +24,7 @@ fn app(cx: Scope) -> Element {
})
}
#[allow(non_snake_case)]
fn Bapp(cx: Scope) -> Element {
cx.render(rsx! {
div {

View file

@ -7,7 +7,7 @@
//! - tests to ensure dyn_into works for various event types.
//! - Partial delegation?>
use dioxus_core::{DomEdit, ElementId, SchedulerMsg, UserEvent};
use dioxus_core::{DomEdit, SchedulerMsg, UserEvent};
use dioxus_html::event_bubbles;
use dioxus_interpreter_js::Interpreter;
use js_sys::Function;
@ -43,7 +43,7 @@ impl WebsysDom {
break Ok(UserEvent {
name: event_name_from_typ(&typ),
data: virtual_event_from_websys_event(event.clone(), target.clone()),
element: Some(ElementId(id)),
element: Some(id),
scope_id: None,
priority: dioxus_core::EventPriority::Medium,
bubbles: event.bubbles(),
@ -157,6 +157,45 @@ impl WebsysDom {
let value = serde_wasm_bindgen::to_value(&value).unwrap();
self.interpreter.SetAttribute(root, field, value, ns)
}
DomEdit::CreateTemplateRef { id, template_id } => {
self.interpreter.CreateTemplateRef(id, template_id)
}
DomEdit::CreateTemplate { id } => self.interpreter.CreateTemplate(id),
DomEdit::FinishTemplate { len } => self.interpreter.FinishTemplate(len),
DomEdit::EnterTemplateRef { root } => self.interpreter.EnterTemplateRef(root),
DomEdit::ExitTemplateRef {} => self.interpreter.ExitTemplateRef(),
DomEdit::CreateTextNodeTemplate {
root,
text,
locally_static,
} => self
.interpreter
.CreateTextNodeTemplate(text, root, locally_static),
DomEdit::CreateElementTemplate {
root,
tag,
locally_static,
fully_static,
} => {
self.interpreter
.CreateElementTemplate(tag, root, locally_static, fully_static)
}
DomEdit::CreateElementNsTemplate {
root,
tag,
ns,
locally_static,
fully_static,
} => self.interpreter.CreateElementNsTemplate(
tag,
root,
ns,
locally_static,
fully_static,
),
DomEdit::CreatePlaceholderTemplate { root } => {
self.interpreter.CreatePlaceholderTemplate(root)
}
}
}
}

View file

@ -1,12 +1,9 @@
use dioxus_core::SchedulerMsg;
use dioxus_core::SetTemplateMsg;
use dioxus_core::VirtualDom;
use dioxus_rsx_interpreter::error::Error;
use dioxus_rsx_interpreter::{ErrorHandler, SetManyRsxMessage, RSX_CONTEXT};
use futures_channel::mpsc::unbounded;
use futures_channel::mpsc::UnboundedSender;
use futures_util::StreamExt;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
use web_sys::{console, MessageEvent, WebSocket};
use web_sys::{MessageEvent, WebSocket};
pub(crate) fn init(dom: &VirtualDom) {
let window = web_sys::window().unwrap();
@ -23,46 +20,18 @@ pub(crate) fn init(dom: &VirtualDom) {
);
let ws = WebSocket::new(&url).unwrap();
let mut channel = dom.get_scheduler_channel();
// change the rsx when new data is received
let cl = Closure::wrap(Box::new(|e: MessageEvent| {
let cl = Closure::wrap(Box::new(move |e: MessageEvent| {
if let Ok(text) = e.data().dyn_into::<js_sys::JsString>() {
let msgs: SetManyRsxMessage = serde_json::from_str(&format!("{text}")).unwrap();
RSX_CONTEXT.extend(msgs);
let msg: SetTemplateMsg = serde_json::from_str(&format!("{text}")).unwrap();
channel
.start_send(SchedulerMsg::SetTemplate(Box::new(msg)))
.unwrap();
}
}) as Box<dyn FnMut(MessageEvent)>);
ws.set_onmessage(Some(cl.as_ref().unchecked_ref()));
cl.forget();
let (error_channel_sender, mut error_channel_receiver) = unbounded();
struct WebErrorHandler {
sender: UnboundedSender<Error>,
}
impl ErrorHandler for WebErrorHandler {
fn handle_error(&self, err: dioxus_rsx_interpreter::error::Error) {
self.sender.unbounded_send(err).unwrap();
}
}
RSX_CONTEXT.set_error_handler(WebErrorHandler {
sender: error_channel_sender,
});
RSX_CONTEXT.provide_scheduler_channel(dom.get_scheduler_channel());
// forward stream to the websocket
dom.base_scope().spawn_forever(async move {
while let Some(err) = error_channel_receiver.next().await {
if ws.ready_state() == WebSocket::OPEN {
ws.send_with_str(serde_json::to_string(&err).unwrap().as_str())
.unwrap();
} else {
console::warn_1(&"WebSocket is not open, cannot send error. Run with dioxus serve --hot-reload to enable hot reloading.".into());
panic!("{}", err);
}
}
});
}

View file

@ -61,15 +61,14 @@ pub use crate::util::use_eval;
use dioxus_core::prelude::Component;
use dioxus_core::SchedulerMsg;
use dioxus_core::VirtualDom;
use futures_util::FutureExt;
mod cache;
mod cfg;
mod dom;
#[cfg(feature = "hot-reload")]
#[cfg(any(feature = "hot-reload", debug_assertions))]
mod hot_reload;
mod rehydrate;
mod ric_raf;
// mod ric_raf;
mod util;
/// Launch the VirtualDOM given a root component and a configuration.
@ -169,7 +168,7 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
console_error_panic_hook::set_once();
}
#[cfg(feature = "hot-reload")]
#[cfg(any(feature = "hot-reload", debug_assertions))]
hot_reload::init(&dom);
for s in crate::cache::BUILTIN_INTERNED_STRINGS {
@ -214,7 +213,7 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
websys_dom.apply_edits(edits.edits);
}
let mut work_loop = ric_raf::RafLoop::new();
// let mut work_loop = ric_raf::RafLoop::new();
loop {
log::trace!("waiting for work");
@ -225,13 +224,14 @@ pub async fn run_with_props<T: 'static + Send>(root: Component<T>, root_props: T
log::trace!("working..");
// wait for the mainthread to schedule us in
let mut deadline = work_loop.wait_for_idle_time().await;
// let mut deadline = work_loop.wait_for_idle_time().await;
// run the virtualdom work phase until the frame deadline is reached
let mutations = dom.work_with_deadline(|| (&mut deadline).now_or_never().is_some());
// let mutations = dom.work_with_deadline(|| (&mut deadline).now_or_never().is_some());
let mutations = dom.work_with_deadline(|| false);
// wait for the animation frame to fire so we can apply our changes
work_loop.wait_for_raf().await;
// work_loop.wait_for_raf().await;
for edit in mutations {
// actually apply our changes during the animation frame

View file

@ -108,12 +108,30 @@ impl WebsysDom {
}
for listener in vel.listeners {
self.interpreter.NewEventListener(
listener.event,
listener.mounted_node.get().unwrap().as_u64(),
self.handler.as_ref().unchecked_ref(),
event_bubbles(listener.event),
);
let global_id = listener.mounted_node.get().unwrap();
match global_id {
dioxus_core::GlobalNodeId::TemplateId {
template_ref_id,
template_node_id: id,
} => {
self.interpreter.EnterTemplateRef(template_ref_id.into());
self.interpreter.NewEventListener(
listener.event,
id.into(),
self.handler.as_ref().unchecked_ref(),
event_bubbles(listener.event),
);
self.interpreter.ExitTemplateRef();
}
dioxus_core::GlobalNodeId::VNodeId(id) => {
self.interpreter.NewEventListener(
listener.event,
id.into(),
self.handler.as_ref().unchecked_ref(),
event_bubbles(listener.event),
);
}
}
}
if !vel.listeners.is_empty() {
@ -165,6 +183,7 @@ impl WebsysDom {
let node = scope.root_node();
self.rehydrate_single(nodes, place, dom, node, last_node_was_text)?;
}
VNode::TemplateRef(_) => todo!(),
}
Ok(())
}