Ver código fonte

create a shadow dom when a custom element is mounted

Evan Almloff 2 anos atrás
pai
commit
788c00274e

+ 54 - 18
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<V: FromAnyValue + Send + Sync> {
-    builders: FxHashMap<&'static str, CustomElementBuilder<V>>,
+    builders: FxHashMap<(&'static str, Option<&'static str>), CustomElementBuilder<V>>,
 }
 
 impl<V: FromAnyValue + Send + Sync> Default for CustomElementRegistry<V> {
@@ -25,7 +29,7 @@ impl<V: FromAnyValue + Send + Sync> Default for CustomElementRegistry<V> {
 impl<V: FromAnyValue + Send + Sync> CustomElementRegistry<V> {
     pub fn register<W: CustomElement<V>>(&mut self) {
         self.builders.insert(
-            W::NAME,
+            (W::NAME, W::NAMESPACE),
             CustomElementBuilder {
                 create: |dom| Box::new(W::create(dom)),
             },
@@ -34,17 +38,26 @@ impl<V: FromAnyValue + Send + Sync> CustomElementRegistry<V> {
 
     pub fn add_shadow_dom(&self, mut node: NodeMut<V>) {
         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<V: FromAnyValue + Send + Sync = ()>: 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<V>) -> Self;
 
     /// The root node of the widget. This must be static once the element is created.
-    fn root(&self) -> NodeId;
+    fn roots(&self) -> Vec<NodeId>;
 
     /// The slot to render children of the element into. This must be static once the element is created.
     fn slot(&self) -> Option<NodeId> {
@@ -76,7 +92,7 @@ pub trait CustomElement<V: FromAnyValue + Send + Sync = ()>: Send + Sync + 'stat
     }
 
     /// Called when the attributes of the widget are changed.
-    fn attributes_changed(&mut self, _dom: &mut RealDom<V>, _attributes: &AttributeMask);
+    fn attributes_changed(&mut self, light_node: NodeMut<V>, attributes: &AttributeMask);
 }
 
 /// A factory for creating widgets
@@ -86,6 +102,9 @@ trait ElementFactory<W: CustomElementUpdater<V>, 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<V>) -> W;
 }
@@ -93,6 +112,8 @@ trait ElementFactory<W: CustomElementUpdater<V>, V: FromAnyValue + Send + Sync =
 impl<W: CustomElement<V>, V: FromAnyValue + Send + Sync> ElementFactory<W, V> for W {
     const NAME: &'static str = W::NAME;
 
+    const NAMESPACE: Option<&'static str> = W::NAMESPACE;
+
     fn create(dom: &mut RealDom<V>) -> Self {
         Self::create(dom)
     }
@@ -101,36 +122,51 @@ impl<W: CustomElement<V>, V: FromAnyValue + Send + Sync> ElementFactory<W, V> fo
 /// A trait for updating widgets
 trait CustomElementUpdater<V: FromAnyValue + Send + Sync = ()>: Send + Sync + 'static {
     /// Called when the attributes of the widget are changed.
-    fn attributes_changed(&mut self, dom: &mut RealDom<V>, attributes: &AttributeMask);
+    fn attributes_changed(&mut self, light_root: NodeMut<V>, attributes: &AttributeMask);
 
     /// The root node of the widget.
-    fn root(&self) -> NodeId;
+    fn roots(&self) -> Vec<NodeId>;
+
+    /// The slot to render children of the element into.
+    fn slot(&self) -> Option<NodeId>;
 }
 
 impl<W: CustomElement<V>, V: FromAnyValue + Send + Sync> CustomElementUpdater<V> for W {
-    fn attributes_changed(&mut self, root: &mut RealDom<V>, attributes: &AttributeMask) {
-        self.attributes_changed(root, attributes);
+    fn attributes_changed(&mut self, light_root: NodeMut<V>, attributes: &AttributeMask) {
+        self.attributes_changed(light_root, attributes);
+    }
+
+    fn roots(&self) -> Vec<NodeId> {
+        self.roots()
     }
 
-    fn root(&self) -> NodeId {
-        self.root()
+    fn slot(&self) -> Option<NodeId> {
+        self.slot()
     }
 }
 
+/// A concrete structure for managing a any widget.
 #[derive(Component, Clone)]
 pub struct CustomElementManager<V: FromAnyValue = ()> {
     inner: Arc<RwLock<Box<dyn CustomElementUpdater<V>>>>,
 }
 
 impl<V: FromAnyValue + Send + Sync> CustomElementManager<V> {
-    pub fn root(&self) -> NodeId {
-        self.inner.read().unwrap().root()
+    /// The root node of the widget's shadow DOM.
+    pub fn roots(&self) -> Vec<NodeId> {
+        self.inner.read().unwrap().roots()
+    }
+
+    /// The slot to render children of the element into.
+    pub fn slot(&self) -> Option<NodeId> {
+        self.inner.read().unwrap().slot()
     }
 
-    pub fn on_attributes_changed(&self, dom: &mut RealDom<V>, attributes: &AttributeMask) {
+    /// Update the custom element based on attributes changed.
+    pub fn on_attributes_changed(&self, light_root: NodeMut<V>, attributes: &AttributeMask) {
         self.inner
             .write()
             .unwrap()
-            .attributes_changed(dom, attributes);
+            .attributes_changed(light_root, attributes);
     }
 }

+ 3 - 3
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),

+ 4 - 5
packages/native-core/src/real_dom.rs

@@ -304,12 +304,11 @@ impl<V: FromAnyValue + Send + Sync> RealDom<V> {
                 };
 
                 // call custom element watchers
-                let custom_element_manager = {
-                    let node = self.get_mut(*node_id).unwrap();
-                    node.get::<CustomElementManager<V>>().map(|x| x.clone())
-                };
+                let node = self.get_mut(*node_id).unwrap();
+                let custom_element_manager =
+                    node.get::<CustomElementManager<V>>().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());
                 }
             }
         }

+ 1 - 1
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,

+ 231 - 68
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: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
+        parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
+        children: Vec<<Self::ChildDependencies as Dependancy>::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,
+        _: <Self::NodeDependencies as Dependancy>::ElementBorrowed<'a>,
+        parent: Option<<Self::ParentDependencies as Dependancy>::ElementBorrowed<'a>>,
+        _: Vec<<Self::ChildDependencies as Dependancy>::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 {
-
         };
     }
 }
 
-#[test]
-fn native_core_is_okay() {
-    use std::sync::{Arc, Mutex};
-    use std::time::Duration;
+struct CustomElementWithSlot {
+    root: NodeId,
+    slot: NodeId,
+}
 
-    fn app(cx: Scope) -> Element {
-        let colors = use_state(cx, || vec!["green", "blue", "red"]);
-        let padding = use_state(cx, || 10);
+impl CustomElement for CustomElementWithSlot {
+    const NAME: &'static str = "customelementslot";
 
-        use_effect(cx, colors, |colors| async move {
-            sleep(Duration::from_millis(1000)).await;
-            colors.with_mut(|colors| colors.reverse());
+    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);
 
-        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;
-                }
-            });
+        Self {
+            root: root.id(),
+            slot: slot_id,
+        }
+    }
+
+    fn slot(&self) -> Option<NodeId> {
+        Some(self.slot)
+    }
+
+    fn roots(&self) -> Vec<NodeId> {
+        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<NodeId> {
+        vec![self.root]
+    }
 
-        let _big = colors[0];
-        let _mid = colors[1];
-        let _small = colors[2];
+    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 custom_elements_work() {
+    fn app(cx: Scope) -> Element {
+        let count = use_state(cx, || 0);
+
+        use_future!(cx, |count| async move {
+            count.with_mut(|count| *count += 1);
+        });
 
         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::<TestElement>();
-        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::<CustomElementWithSlot>();
+        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::<ColorState>().unwrap();
+                let size = *node.get::<LayoutState>().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 {}
+            }
+        })
+    }
 
-impl CustomElement for TestElement {
-    const NAME: &'static str = "blabla";
+    let rt = tokio::runtime::Builder::new_current_thread()
+        .enable_time()
+        .build()
+        .unwrap();
 
-    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() }
-    }
+    rt.block_on(async {
+        let mut rdom = RealDom::new([LayoutState::to_type_erased(), ColorState::to_type_erased()]);
+        rdom.register_custom_element::<CustomElementWithNoSlot>();
+        let mut dioxus_state = DioxusState::create(&mut rdom);
+        let mut dom = VirtualDom::new(app);
 
-    fn root(&self) -> NodeId {
-        self.root
-    }
+        let mutations = dom.rebuild();
+        dioxus_state.apply_mutations(&mut rdom, mutations);
 
-    fn attributes_changed(
-        &mut self,
-        _dom: &mut RealDom<()>,
-        attributes: &dioxus_native_core::node_ref::AttributeMask,
-    ) {
-        println!("attributes_changed");
-        println!("{:?}", attributes);
+        let ctx = SendAnyMap::new();
+        rdom.update_state(ctx);
+    });
+}
+
+#[test]
+fn slotless_custom_element() {
+    fn app(cx: Scope) -> Element {
+        cx.render(rsx! {
+            customelementnoslot {
+            }
+        })
     }
+
+    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::<CustomElementWithNoSlot>();
+        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);
+    });
 }