From 788c00274eb1ba6fe3136a31c21028fa64556599 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 12 Apr 2023 10:05:09 -0500 Subject: [PATCH] create a shadow dom when a custom element is mounted --- packages/native-core/src/custom_element.rs | 72 +++-- packages/native-core/src/node_ref.rs | 6 +- packages/native-core/src/real_dom.rs | 9 +- packages/native-core/src/tree.rs | 2 +- packages/native-core/tests/custom_element.rs | 303 ++++++++++++++----- 5 files changed, 295 insertions(+), 97 deletions(-) diff --git a/packages/native-core/src/custom_element.rs b/packages/native-core/src/custom_element.rs index f829d89f0..62d7e5368 100644 --- a/packages/native-core/src/custom_element.rs +++ b/packages/native-core/src/custom_element.rs @@ -1,3 +1,6 @@ +//! A custom element is a controlled element that renders to a shadow DOM. +//! Each custom element is registered with a element name + use std::sync::{Arc, RwLock}; use rustc_hash::FxHashMap; @@ -7,11 +10,12 @@ use crate::{ node::{FromAnyValue, NodeType}, node_ref::AttributeMask, prelude::{NodeImmutable, NodeMut, RealDom}, + tree::TreeMut, NodeId, }; pub(crate) struct CustomElementRegistry { - builders: FxHashMap<&'static str, CustomElementBuilder>, + builders: FxHashMap<(&'static str, Option<&'static str>), CustomElementBuilder>, } impl Default for CustomElementRegistry { @@ -25,7 +29,7 @@ impl Default for CustomElementRegistry { impl CustomElementRegistry { pub fn register>(&mut self) { self.builders.insert( - W::NAME, + (W::NAME, W::NAMESPACE), CustomElementBuilder { create: |dom| Box::new(W::create(dom)), }, @@ -34,17 +38,26 @@ impl CustomElementRegistry { pub fn add_shadow_dom(&self, mut node: NodeMut) { let element_tag = if let NodeType::Element(el) = &*node.node_type() { - Some(el.tag.clone()) + Some((el.tag.clone(), el.namespace.clone())) } else { None }; - if let Some(element_tag) = element_tag { - if let Some(builder) = self.builders.get(element_tag.as_str()) { + if let Some((tag, ns)) = element_tag { + if let Some(builder) = self.builders.get(&(tag.as_str(), ns.as_deref())) { let boxed_widget = { let dom = node.real_dom_mut(); (builder.create)(dom) }; + let shadow_roots = boxed_widget.roots(); + + let light_id = node.id(); + node.real_dom_mut().tree_mut().create_subtree( + light_id, + shadow_roots, + boxed_widget.slot(), + ); + let boxed_widget = CustomElementManager { inner: Arc::new(RwLock::new(boxed_widget)), }; @@ -64,11 +77,14 @@ pub trait CustomElement: Send + Sync + 'stat /// The tag the widget is registered under. const NAME: &'static str; - /// Create a new widget without mounting it. + /// The namespace the widget is registered under. + const NAMESPACE: Option<&'static str> = None; + + /// Create a new widget *without mounting* it. fn create(dom: &mut RealDom) -> Self; /// The root node of the widget. This must be static once the element is created. - fn root(&self) -> NodeId; + fn roots(&self) -> Vec; /// The slot to render children of the element into. This must be static once the element is created. fn slot(&self) -> Option { @@ -76,7 +92,7 @@ pub trait CustomElement: Send + Sync + 'stat } /// Called when the attributes of the widget are changed. - fn attributes_changed(&mut self, _dom: &mut RealDom, _attributes: &AttributeMask); + fn attributes_changed(&mut self, light_node: NodeMut, attributes: &AttributeMask); } /// A factory for creating widgets @@ -86,6 +102,9 @@ trait ElementFactory, V: FromAnyValue + Send + Sync = /// The tag the widget is registered under. const NAME: &'static str; + /// The namespace the widget is registered under. + const NAMESPACE: Option<&'static str> = None; + /// Create a new widget. fn create(dom: &mut RealDom) -> W; } @@ -93,6 +112,8 @@ trait ElementFactory, V: FromAnyValue + Send + Sync = impl, V: FromAnyValue + Send + Sync> ElementFactory for W { const NAME: &'static str = W::NAME; + const NAMESPACE: Option<&'static str> = W::NAMESPACE; + fn create(dom: &mut RealDom) -> Self { Self::create(dom) } @@ -101,36 +122,51 @@ impl, V: FromAnyValue + Send + Sync> ElementFactory fo /// A trait for updating widgets trait CustomElementUpdater: Send + Sync + 'static { /// Called when the attributes of the widget are changed. - fn attributes_changed(&mut self, dom: &mut RealDom, attributes: &AttributeMask); + fn attributes_changed(&mut self, light_root: NodeMut, attributes: &AttributeMask); /// The root node of the widget. - fn root(&self) -> NodeId; + fn roots(&self) -> Vec; + + /// The slot to render children of the element into. + fn slot(&self) -> Option; } impl, V: FromAnyValue + Send + Sync> CustomElementUpdater for W { - fn attributes_changed(&mut self, root: &mut RealDom, attributes: &AttributeMask) { - self.attributes_changed(root, attributes); + fn attributes_changed(&mut self, light_root: NodeMut, attributes: &AttributeMask) { + self.attributes_changed(light_root, attributes); } - fn root(&self) -> NodeId { - self.root() + fn roots(&self) -> Vec { + self.roots() + } + + fn slot(&self) -> Option { + self.slot() } } +/// A concrete structure for managing a any widget. #[derive(Component, Clone)] pub struct CustomElementManager { inner: Arc>>>, } impl CustomElementManager { - pub fn root(&self) -> NodeId { - self.inner.read().unwrap().root() + /// The root node of the widget's shadow DOM. + pub fn roots(&self) -> Vec { + self.inner.read().unwrap().roots() } - pub fn on_attributes_changed(&self, dom: &mut RealDom, attributes: &AttributeMask) { + /// The slot to render children of the element into. + pub fn slot(&self) -> Option { + self.inner.read().unwrap().slot() + } + + /// Update the custom element based on attributes changed. + pub fn on_attributes_changed(&self, light_root: NodeMut, attributes: &AttributeMask) { self.inner .write() .unwrap() - .attributes_changed(dom, attributes); + .attributes_changed(light_root, attributes); } } diff --git a/packages/native-core/src/node_ref.rs b/packages/native-core/src/node_ref.rs index a1a309622..156ecdbd6 100644 --- a/packages/native-core/src/node_ref.rs +++ b/packages/native-core/src/node_ref.rs @@ -60,7 +60,7 @@ impl<'a, V: FromAnyValue> NodeView<'a, V> { NodeType::Element(ElementNode { attributes, .. }) => Some( attributes .iter() - .filter(move |(attr, _)| self.mask.attritutes.contains_attribute(&attr.name)) + .filter(move |(attr, _)| self.mask.attritutes.contains(&attr.name)) .map(|(attr, val)| OwnedAttributeView { attribute: attr, value: val, @@ -107,8 +107,8 @@ pub enum AttributeMask { } impl AttributeMask { - /// Check if the given attribute is visible - fn contains_attribute(&self, attr: &str) -> bool { + /// Check if the mask contains the given attribute + pub fn contains(&self, attr: &str) -> bool { match self { AttributeMask::All => true, AttributeMask::Some(attrs) => attrs.contains(attr), diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 490e04dc6..bdcd10d55 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -304,12 +304,11 @@ impl RealDom { }; // call custom element watchers - let custom_element_manager = { - let node = self.get_mut(*node_id).unwrap(); - node.get::>().map(|x| x.clone()) - }; + let node = self.get_mut(*node_id).unwrap(); + let custom_element_manager = + node.get::>().map(|x| x.clone()); if let Some(custom_element_manager) = custom_element_manager { - custom_element_manager.on_attributes_changed(self, mask.attributes()); + custom_element_manager.on_attributes_changed(node, mask.attributes()); } } } diff --git a/packages/native-core/src/tree.rs b/packages/native-core/src/tree.rs index e0b32f28d..1f46216b2 100644 --- a/packages/native-core/src/tree.rs +++ b/packages/native-core/src/tree.rs @@ -294,7 +294,7 @@ fn child_height(parent: &Node, tree: &impl TreeRef) -> u16 { .expect("Attempted to read a slot that does not exist") + 1 } else { - panic!("Attempted to read the height of a shadow_tree without a slot"); + panic!("Attempted to read the height of a child of a node with a shadow tree, but the shadow tree does not have a slot. Every shadow tree attached to a node with children must have a slot.") } } None => parent.height + 1, diff --git a/packages/native-core/tests/custom_element.rs b/packages/native-core/tests/custom_element.rs index f1285c8cc..3aa1b7462 100644 --- a/packages/native-core/tests/custom_element.rs +++ b/packages/native-core/tests/custom_element.rs @@ -2,21 +2,23 @@ use dioxus::prelude::*; use dioxus_native_core::{custom_element::CustomElement, prelude::*}; use dioxus_native_core_macro::partial_derive_state; use shipyard::Component; -use tokio::time::sleep; -#[derive(Debug, Clone, PartialEq, Eq, Default, Component)] -pub struct BlablaState { - count: usize, +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Component)] +pub struct ColorState { + color: usize, } #[partial_derive_state] -impl State for BlablaState { +impl State for ColorState { type ParentDependencies = (Self,); type ChildDependencies = (); type NodeDependencies = (); + // The color state should not be effected by the shadow dom + const TRAVERSE_SHADOW_DOM: bool = false; + const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new() - .with_attrs(AttributeMaskBuilder::Some(&["blabla"])) + .with_attrs(AttributeMaskBuilder::Some(&["color"])) .with_element(); fn update<'a>( @@ -28,8 +30,70 @@ impl State for BlablaState { _: &SendAnyMap, ) -> bool { if let Some((parent,)) = parent { - if parent.count != 0 { - self.count += 1; + self.color = parent.color; + } + true + } + + fn create<'a>( + node_view: NodeView<()>, + node: ::ElementBorrowed<'a>, + parent: Option<::ElementBorrowed<'a>>, + children: Vec<::ElementBorrowed<'a>>, + context: &SendAnyMap, + ) -> Self { + let mut myself = Self::default(); + myself.update(node_view, node, parent, children, context); + myself + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Component)] +pub struct LayoutState { + size: usize, +} + +#[partial_derive_state] +impl State for LayoutState { + type ParentDependencies = (Self,); + type ChildDependencies = (); + type NodeDependencies = (); + + // The layout state should be effected by the shadow dom + const TRAVERSE_SHADOW_DOM: bool = true; + + const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new() + .with_attrs(AttributeMaskBuilder::Some(&["size"])) + .with_element(); + + fn update<'a>( + &mut self, + view: NodeView, + _: ::ElementBorrowed<'a>, + parent: Option<::ElementBorrowed<'a>>, + _: Vec<::ElementBorrowed<'a>>, + _: &SendAnyMap, + ) -> bool { + println!( + "Updating layout state @{:?} {:?}", + parent.as_ref().map(|(p,)| p.size), + view.node_id() + ); + if let Some(size) = view + .attributes() + .into_iter() + .flatten() + .find(|attr| attr.attribute.name == "size") + { + self.size = size + .value + .as_float() + .or_else(|| size.value.as_int().map(|i| i as f64)) + .or_else(|| size.value.as_text().and_then(|i| i.parse().ok())) + .unwrap_or(0.0) as usize; + } else if let Some((parent,)) = parent { + if parent.size > 0 { + self.size -= 1; } } true @@ -66,6 +130,7 @@ mod dioxus_elements { $(#[$attr])* pub struct $name; + #[allow(non_upper_case_globals)] impl $name { pub const TAG_NAME: &'static str = stringify!($name); pub const NAME_SPACE: Option<&'static str> = None; @@ -85,47 +150,110 @@ mod dioxus_elements { pub trait SvgAttributes {} builder_constructors! { - blabla { - + customelementslot { + size: attr, + }; + customelementnoslot { }; testing132 { - }; } } +struct CustomElementWithSlot { + root: NodeId, + slot: NodeId, +} + +impl CustomElement for CustomElementWithSlot { + const NAME: &'static str = "customelementslot"; + + fn create(dom: &mut RealDom<()>) -> Self { + let child = dom.create_node(ElementNode { + tag: "div".into(), + namespace: None, + attributes: Default::default(), + listeners: Default::default(), + }); + let slot_id = child.id(); + let mut root = dom.create_node(ElementNode { + tag: "div".into(), + namespace: None, + attributes: Default::default(), + listeners: Default::default(), + }); + root.add_child(slot_id); + + Self { + root: root.id(), + slot: slot_id, + } + } + + fn slot(&self) -> Option { + Some(self.slot) + } + + fn roots(&self) -> Vec { + vec![self.root] + } + + fn attributes_changed( + &mut self, + node: NodeMut<()>, + attributes: &dioxus_native_core::node_ref::AttributeMask, + ) { + println!("attributes_changed"); + println!("{:?}", attributes); + println!("{:?}: {:#?}", node.id(), &*node.node_type()); + } +} + +struct CustomElementWithNoSlot { + root: NodeId, +} + +impl CustomElement for CustomElementWithNoSlot { + const NAME: &'static str = "customelementnoslot"; + + fn create(dom: &mut RealDom<()>) -> Self { + let root = dom.create_node(ElementNode { + tag: "div".into(), + namespace: None, + attributes: Default::default(), + listeners: Default::default(), + }); + Self { root: root.id() } + } + + fn roots(&self) -> Vec { + vec![self.root] + } + + fn attributes_changed( + &mut self, + node: NodeMut<()>, + attributes: &dioxus_native_core::node_ref::AttributeMask, + ) { + println!("attributes_changed"); + println!("{:?}", attributes); + println!("{:?}: {:#?}", node.id(), &*node.node_type()); + } +} + #[test] -fn native_core_is_okay() { - use std::sync::{Arc, Mutex}; - use std::time::Duration; - +fn custom_elements_work() { fn app(cx: Scope) -> Element { - let colors = use_state(cx, || vec!["green", "blue", "red"]); - let padding = use_state(cx, || 10); + let count = use_state(cx, || 0); - use_effect(cx, colors, |colors| async move { - sleep(Duration::from_millis(1000)).await; - colors.with_mut(|colors| colors.reverse()); + use_future!(cx, |count| async move { + count.with_mut(|count| *count += 1); }); - use_effect(cx, padding, |padding| async move { - sleep(Duration::from_millis(10)).await; - padding.with_mut(|padding| { - if *padding < 65 { - *padding += 1; - } else { - *padding = 5; - } - }); - }); - - let _big = colors[0]; - let _mid = colors[1]; - let _small = colors[2]; - cx.render(rsx! { - blabla { - blabla { + customelementslot { + size: "{count}", + customelementslot { testing132 {} } } @@ -138,58 +266,93 @@ fn native_core_is_okay() { .unwrap(); rt.block_on(async { - let rdom = Arc::new(Mutex::new(RealDom::new([BlablaState::to_type_erased()]))); - rdom.lock() - .unwrap() - .register_custom_element::(); - let mut dioxus_state = DioxusState::create(&mut rdom.lock().unwrap()); + let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]); + rdom.register_custom_element::(); + let mut dioxus_state = DioxusState::create(&mut rdom); let mut dom = VirtualDom::new(app); let mutations = dom.rebuild(); - dioxus_state.apply_mutations(&mut rdom.lock().unwrap(), mutations); + dioxus_state.apply_mutations(&mut rdom, mutations); let ctx = SendAnyMap::new(); - rdom.lock().unwrap().update_state(ctx); + rdom.update_state(ctx); for _ in 0..10 { dom.wait_for_work().await; let mutations = dom.render_immediate(); - dioxus_state.apply_mutations(&mut rdom.lock().unwrap(), mutations); + dioxus_state.apply_mutations(&mut rdom, mutations); let ctx = SendAnyMap::new(); - rdom.lock().unwrap().update_state(ctx); + rdom.update_state(ctx); + + // render... + rdom.traverse_depth_first(|node| { + let node_type = &*node.node_type(); + let indent = " ".repeat(node.height() as usize); + let color = *node.get::().unwrap(); + let size = *node.get::().unwrap(); + let id = node.id(); + println!("{indent}{id:?} {color:?} {size:?} {node_type:?}"); + }); } }); } -struct TestElement { - root: NodeId, +#[test] +#[should_panic] +fn slotless_custom_element_cant_have_children() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + customelementnoslot { + testing132 {} + } + }) + } + + let rt = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .unwrap(); + + rt.block_on(async { + let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]); + rdom.register_custom_element::(); + let mut dioxus_state = DioxusState::create(&mut rdom); + let mut dom = VirtualDom::new(app); + + let mutations = dom.rebuild(); + dioxus_state.apply_mutations(&mut rdom, mutations); + + let ctx = SendAnyMap::new(); + rdom.update_state(ctx); + }); } -impl CustomElement for TestElement { - const NAME: &'static str = "blabla"; - - fn create(dom: &mut RealDom<()>) -> Self { - let root = dom.create_node(ElementNode { - tag: "shadow_root".into(), - namespace: None, - attributes: Default::default(), - listeners: Default::default(), - }); - Self { root: root.id() } +#[test] +fn slotless_custom_element() { + fn app(cx: Scope) -> Element { + cx.render(rsx! { + customelementnoslot { + } + }) } - fn root(&self) -> NodeId { - self.root - } + let rt = tokio::runtime::Builder::new_current_thread() + .enable_time() + .build() + .unwrap(); - fn attributes_changed( - &mut self, - _dom: &mut RealDom<()>, - attributes: &dioxus_native_core::node_ref::AttributeMask, - ) { - println!("attributes_changed"); - println!("{:?}", attributes); - } + rt.block_on(async { + let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]); + rdom.register_custom_element::(); + let mut dioxus_state = DioxusState::create(&mut rdom); + let mut dom = VirtualDom::new(app); + + let mutations = dom.rebuild(); + dioxus_state.apply_mutations(&mut rdom, mutations); + + let ctx = SendAnyMap::new(); + rdom.update_state(ctx); + }); }