diff --git a/packages/native-core-macro/src/lib.rs b/packages/native-core-macro/src/lib.rs index fd25c3868..2c8cef835 100644 --- a/packages/native-core-macro/src/lib.rs +++ b/packages/native-core-macro/src/lib.rs @@ -325,7 +325,7 @@ pub fn partial_derive_state(_: TokenStream, input: TokenStream) -> TokenStream { #(#items)* - fn workload_system(type_id: std::any::TypeId, dependants: dioxus_native_core::exports::FxHashSet, pass_direction: dioxus_native_core::prelude::PassDirection) -> dioxus_native_core::exports::shipyard::WorkloadSystem { + fn workload_system(type_id: std::any::TypeId, dependants: std::sync::Arc, pass_direction: dioxus_native_core::prelude::PassDirection) -> dioxus_native_core::exports::shipyard::WorkloadSystem { use dioxus_native_core::exports::shipyard::{IntoWorkloadSystem, Get, AddComponent}; use dioxus_native_core::tree::TreeRef; use dioxus_native_core::prelude::{NodeType, NodeView}; diff --git a/packages/native-core/examples/font_size.rs b/packages/native-core/examples/font_size.rs new file mode 100644 index 000000000..937db0c17 --- /dev/null +++ b/packages/native-core/examples/font_size.rs @@ -0,0 +1,175 @@ +use dioxus_native_core::exports::shipyard::Component; +use dioxus_native_core::node_ref::*; +use dioxus_native_core::prelude::*; +use dioxus_native_core::real_dom::NodeTypeMut; +use dioxus_native_core_macro::partial_derive_state; + +// All states need to derive Component +#[derive(Default, Debug, Copy, Clone, Component)] +struct Size(f64, f64); + +/// Derive some of the boilerplate for the State implementation +#[partial_derive_state] +impl State for Size { + type ParentDependencies = (); + + // The size of the current node depends on the size of its children + type ChildDependencies = (Self,); + + type NodeDependencies = (FontSize,); + + // Size only cares about the width, height, and text parts of the current node + const NODE_MASK: NodeMaskBuilder<'static> = NodeMaskBuilder::new() + // Get access to the width and height attributes + .with_attrs(AttributeMaskBuilder::Some(&["width", "height"])) + // Get access to the text of the node + .with_text(); + + fn update<'a>( + &mut self, + node_view: NodeView<()>, + (font_size,): ::ElementBorrowed<'a>, + _parent: Option<::ElementBorrowed<'a>>, + children: Vec<::ElementBorrowed<'a>>, + _: &SendAnyMap, + ) -> bool { + let font_size = font_size.size; + let mut width; + let mut height; + if let Some(text) = node_view.text() { + // if the node has text, use the text to size our object + width = text.len() as f64 * font_size; + height = font_size; + } else { + // otherwise, the size is the maximum size of the children + width = children + .iter() + .map(|(item,)| item.0) + .reduce(|accum, item| if accum >= item { accum } else { item }) + .unwrap_or(0.0); + + height = children + .iter() + .map(|(item,)| item.1) + .reduce(|accum, item| if accum >= item { accum } else { item }) + .unwrap_or(0.0); + } + // if the node contains a width or height attribute it overrides the other size + for a in node_view.attributes().into_iter().flatten() { + match &*a.attribute.name { + "width" => width = a.value.as_float().unwrap(), + "height" => height = a.value.as_float().unwrap(), + // because Size only depends on the width and height, no other attributes will be passed to the member + _ => panic!(), + } + } + // to determine what other parts of the dom need to be updated we return a boolean that marks if this member changed + let changed = (width != self.0) || (height != self.1); + *self = Self(width, height); + changed + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Component)] +struct FontSize { + size: f64, +} + +impl Default for FontSize { + fn default() -> Self { + Self { size: 16.0 } + } +} + +#[partial_derive_state] +impl State for FontSize { + // TextColor depends on the TextColor part of the parent + type ParentDependencies = (Self,); + + type ChildDependencies = (); + + type NodeDependencies = (); + + // TextColor only cares about the color attribute of the current node + const NODE_MASK: NodeMaskBuilder<'static> = + // Get access to the color attribute + NodeMaskBuilder::new().with_attrs(AttributeMaskBuilder::Some(&["font-size"])); + + fn update<'a>( + &mut self, + node_view: NodeView<()>, + _node: ::ElementBorrowed<'a>, + parent: Option<::ElementBorrowed<'a>>, + _children: Vec<::ElementBorrowed<'a>>, + _context: &SendAnyMap, + ) -> bool { + let mut new = None; + for attr in node_view.attributes().into_iter().flatten() { + if attr.attribute.name == "font-size" { + new = Some(FontSize { + size: attr.value.as_float().unwrap(), + }); + } + } + let new = new.unwrap_or(parent.map(|(p,)| *p).unwrap_or_default()); + // check if the member has changed + let changed = new != *self; + *self = new; + changed + } +} + +fn main() -> Result<(), Box> { + let mut rdom: RealDom = RealDom::new([FontSize::to_type_erased(), Size::to_type_erased()]); + + let mut count = 0; + + // intial render + let text_id = rdom.create_node(format!("Count: {count}")).id(); + let mut root = rdom.get_mut(rdom.root_id()).unwrap(); + // set the color to red + if let NodeTypeMut::Element(mut element) = root.node_type_mut() { + element.set_attribute(("color", "style"), "red".to_string()); + element.set_attribute(("font-size", "style"), 1.); + } + root.add_child(text_id); + + let ctx = SendAnyMap::new(); + // update the State for nodes in the real_dom tree + let _to_rerender = rdom.update_state(ctx); + + // we need to run the vdom in a async runtime + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build()? + .block_on(async { + loop { + // update the count and font size + count += 1; + let mut text = rdom.get_mut(text_id).unwrap(); + if let NodeTypeMut::Text(mut text) = text.node_type_mut() { + *text = format!("Count: {count}"); + } + if let NodeTypeMut::Element(mut element) = + rdom.get_mut(rdom.root_id()).unwrap().node_type_mut() + { + element.set_attribute(("font-size", "style"), count as f64); + } + + let ctx = SendAnyMap::new(); + let _to_rerender = rdom.update_state(ctx); + + // render... + rdom.traverse_depth_first(|node| { + let indent = " ".repeat(node.height() as usize); + let font_size = *node.get::().unwrap(); + let size = *node.get::().unwrap(); + let id = node.id(); + println!("{indent}{id:?} {font_size:?} {size:?}"); + }); + + // wait 1 second + tokio::time::sleep(std::time::Duration::from_secs(1)).await; + } + }) +} diff --git a/packages/native-core/src/lib.rs b/packages/native-core/src/lib.rs index ca8e8e5ee..0740d881a 100644 --- a/packages/native-core/src/lib.rs +++ b/packages/native-core/src/lib.rs @@ -35,7 +35,7 @@ pub mod prelude { pub use crate::node::{ElementNode, FromAnyValue, NodeType, OwnedAttributeView, TextNode}; pub use crate::node_ref::{AttributeMaskBuilder, NodeMaskBuilder, NodeView}; pub use crate::passes::{run_pass, PassDirection, RunPassView, TypeErasedState}; - pub use crate::passes::{Dependancy, DependancyView, State}; + pub use crate::passes::{Dependancy, DependancyView, Dependants, State}; pub use crate::real_dom::{NodeImmutable, NodeMut, NodeRef, RealDom}; pub use crate::NodeId; pub use crate::SendAnyMap; diff --git a/packages/native-core/src/passes.rs b/packages/native-core/src/passes.rs index 2a84e6df4..b453e32dd 100644 --- a/packages/native-core/src/passes.rs +++ b/packages/native-core/src/passes.rs @@ -126,7 +126,7 @@ pub trait State: Any + Send + Sync { /// Create a workload system for this state fn workload_system( type_id: TypeId, - dependants: FxHashSet, + dependants: Arc, pass_direction: PassDirection, ) -> WorkloadSystem; @@ -138,10 +138,16 @@ pub trait State: Any + Send + Sync { let node_mask = Self::NODE_MASK.build(); TypeErasedState { this_type_id: TypeId::of::(), - combined_dependancy_type_ids: all_dependanices::().iter().copied().collect(), - parent_dependant: !Self::ParentDependencies::type_ids().is_empty(), - child_dependant: !Self::ChildDependencies::type_ids().is_empty(), - dependants: FxHashSet::default(), + parent_dependancies_ids: Self::ParentDependencies::type_ids() + .iter() + .copied() + .collect(), + child_dependancies_ids: Self::ChildDependencies::type_ids() + .iter() + .copied() + .collect(), + node_dependancies_ids: Self::NodeDependencies::type_ids().iter().copied().collect(), + dependants: Default::default(), mask: node_mask, pass_direction: pass_direction::(), workload: Self::workload_system, @@ -166,13 +172,6 @@ fn pass_direction>() -> PassDirection } } -fn all_dependanices>() -> Box<[TypeId]> { - let mut dependencies = S::ParentDependencies::type_ids().to_vec(); - dependencies.extend(S::ChildDependencies::type_ids().iter()); - dependencies.extend(S::NodeDependencies::type_ids().iter()); - dependencies.into_boxed_slice() -} - #[doc(hidden)] #[derive(Borrow, BorrowInfo)] pub struct RunPassView<'a, V: FromAnyValue + Send + Sync = ()> { @@ -188,7 +187,7 @@ pub struct RunPassView<'a, V: FromAnyValue + Send + Sync = ()> { #[doc(hidden)] pub fn run_pass( type_id: TypeId, - dependants: FxHashSet, + dependants: Arc, pass_direction: PassDirection, view: RunPassView, mut update_node: impl FnMut(NodeId, &SendAnyMap) -> bool, @@ -206,11 +205,7 @@ pub fn run_pass( while let Some((height, id)) = dirty.pop_front(type_id) { if (update_node)(id, ctx) { nodes_updated.insert(id); - for id in tree.children_ids(id) { - for dependant in &dependants { - dirty.insert(*dependant, id, height + 1); - } - } + dependants.mark_dirty(&dirty, id, &tree, height); } } } @@ -218,11 +213,7 @@ pub fn run_pass( while let Some((height, id)) = dirty.pop_back(type_id) { if (update_node)(id, ctx) { nodes_updated.insert(id); - if let Some(id) = tree.parent_id(id) { - for dependant in &dependants { - dirty.insert(*dependant, id, height - 1); - } - } + dependants.mark_dirty(&dirty, id, &tree, height); } } } @@ -230,24 +221,53 @@ pub fn run_pass( while let Some((height, id)) = dirty.pop_back(type_id) { if (update_node)(id, ctx) { nodes_updated.insert(id); - for dependant in &dependants { - dirty.insert(*dependant, id, height); - } + dependants.mark_dirty(&dirty, id, &tree, height); } } } } } +/// The states that depend on this state +#[derive(Default, Debug, Clone, PartialEq, Eq)] +pub struct Dependants { + /// The states in the parent direction that should be invalidated when this state is invalidated + pub parent: Vec, + /// The states in the child direction that should be invalidated when this state is invalidated + pub child: Vec, + /// The states in the node direction that should be invalidated when this state is invalidated + pub node: Vec, +} + +impl Dependants { + fn mark_dirty(&self, dirty: &DirtyNodeStates, id: NodeId, tree: &impl TreeRef, height: u16) { + for dependant in &self.child { + for id in tree.children_ids(id) { + dirty.insert(*dependant, id, height + 1); + } + } + + for dependant in &self.parent { + if let Some(id) = tree.parent_id(id) { + dirty.insert(*dependant, id, height - 1); + } + } + + for dependant in &self.node { + dirty.insert(*dependant, id, height); + } + } +} + /// A type erased version of [`State`] that can be added to the [`crate::prelude::RealDom`] with [`crate::prelude::RealDom::new`] pub struct TypeErasedState { pub(crate) this_type_id: TypeId, - pub(crate) parent_dependant: bool, - pub(crate) child_dependant: bool, - pub(crate) combined_dependancy_type_ids: FxHashSet, - pub(crate) dependants: FxHashSet, + pub(crate) parent_dependancies_ids: FxHashSet, + pub(crate) child_dependancies_ids: FxHashSet, + pub(crate) node_dependancies_ids: FxHashSet, + pub(crate) dependants: Arc, pub(crate) mask: NodeMask, - pub(crate) workload: fn(TypeId, FxHashSet, PassDirection) -> WorkloadSystem, + pub(crate) workload: fn(TypeId, Arc, PassDirection) -> WorkloadSystem, pub(crate) pass_direction: PassDirection, phantom: PhantomData, } @@ -260,10 +280,18 @@ impl TypeErasedState { self.pass_direction, ) } + + pub(crate) fn combined_dependancy_type_ids(&self) -> impl Iterator + '_ { + self.parent_dependancies_ids + .iter() + .chain(self.child_dependancies_ids.iter()) + .chain(self.node_dependancies_ids.iter()) + .copied() + } } /// The direction that a pass should be run in -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum PassDirection { /// The pass should be run from the root to the leaves ParentToChild, diff --git a/packages/native-core/src/real_dom.rs b/packages/native-core/src/real_dom.rs index 25d1ed9fe..c344b9041 100644 --- a/packages/native-core/src/real_dom.rs +++ b/packages/native-core/src/real_dom.rs @@ -15,7 +15,7 @@ use crate::node::{ }; use crate::node_ref::{NodeMask, NodeMaskBuilder}; use crate::node_watcher::NodeWatcher; -use crate::passes::{DirtyNodeStates, TypeErasedState}; +use crate::passes::{DirtyNodeStates, PassDirection, TypeErasedState}; use crate::prelude::AttributeMaskBuilder; use crate::tree::{TreeMut, TreeMutView, TreeRef, TreeRefView}; use crate::NodeId; @@ -68,12 +68,13 @@ impl NodesDirty { } } - /// Mark a node as added or removed from the tree + /// Mark a node that has had a parent changed fn mark_parent_added_or_removed(&mut self, node_id: NodeId) { let hm = self.passes_updated.entry(node_id).or_default(); for pass in &*self.passes { - if pass.parent_dependant { - hm.insert(pass.this_type_id); + // If any of the states in this node depend on the parent then mark them as dirty + for &pass in &pass.parent_dependancies_ids { + hm.insert(pass); } } } @@ -82,8 +83,9 @@ impl NodesDirty { fn mark_child_changed(&mut self, node_id: NodeId) { let hm = self.passes_updated.entry(node_id).or_default(); for pass in &*self.passes { - if pass.child_dependant { - hm.insert(pass.this_type_id); + // If any of the states in this node depend on the children then mark them as dirty + for &pass in &pass.child_dependancies_ids { + hm.insert(pass); } } } @@ -116,16 +118,46 @@ impl RealDom { pub fn new(tracked_states: impl Into]>>) -> RealDom { let mut tracked_states = tracked_states.into(); // resolve dependants for each pass - for i in 1..tracked_states.len() { + for i in 1..=tracked_states.len() { let (before, after) = tracked_states.split_at_mut(i); let (current, before) = before.split_last_mut().unwrap(); - for pass in before.iter_mut().chain(after.iter_mut()) { + for state in before.iter_mut().chain(after.iter_mut()) { + let dependants = Arc::get_mut(&mut state.dependants).unwrap(); + // If this node depends on the other state as a parent, then the other state should update its children of the current type when it is invalidated if current - .combined_dependancy_type_ids - .contains(&pass.this_type_id) + .parent_dependancies_ids + .contains(&state.this_type_id) + && !dependants.child.contains(¤t.this_type_id) { - pass.dependants.insert(current.this_type_id); + dependants.child.push(current.this_type_id); } + // If this node depends on the other state as a child, then the other state should update its parent of the current type when it is invalidated + if current.child_dependancies_ids.contains(&state.this_type_id) + && !dependants.parent.contains(¤t.this_type_id) + { + dependants.parent.push(current.this_type_id); + } + // If this node depends on the other state as a sibling, then the other state should update its siblings of the current type when it is invalidated + if current.node_dependancies_ids.contains(&state.this_type_id) + && !dependants.node.contains(¤t.this_type_id) + { + dependants.node.push(current.this_type_id); + } + } + // If the current state depends on itself, then it should update itself when it is invalidated + let dependants = Arc::get_mut(&mut current.dependants).unwrap(); + match current.pass_direction { + PassDirection::ChildToParent => { + if !dependants.parent.contains(¤t.this_type_id) { + dependants.parent.push(current.this_type_id); + } + } + PassDirection::ParentToChild => { + if !dependants.child.contains(¤t.this_type_id) { + dependants.child.push(current.this_type_id); + } + } + _ => {} } } let workload = construct_workload(&mut tracked_states); @@ -1011,7 +1043,8 @@ fn construct_workload( // mark any dependancies for i in 0..unresloved_workloads.len() { let (_, pass, _) = &unresloved_workloads[i]; - for ty_id in pass.combined_dependancy_type_ids.clone() { + let all_dependancies: Vec<_> = pass.combined_dependancy_type_ids().collect(); + for ty_id in all_dependancies { let &(dependancy_id, _, _) = unresloved_workloads .iter() .find(|(_, pass, _)| pass.this_type_id == ty_id)