diff --git a/benches/Cargo.toml b/benches/Cargo.toml index bc50f7b50..57c02bcf3 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -51,3 +51,8 @@ harness = false name = "mixed" path = "benches/mixed.rs" harness = false + +[[bench]] +name = "display_contents" +path = "benches/display_contents.rs" +harness = false diff --git a/benches/benches/display_contents.rs b/benches/benches/display_contents.rs new file mode 100644 index 000000000..51a8a34d2 --- /dev/null +++ b/benches/benches/display_contents.rs @@ -0,0 +1,171 @@ +//! Benchmarks for Display::Contents overhead +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use taffy::prelude::*; +use taffy::style::Style; + +/// Build a flat flex tree with N children, no contents nodes. +fn build_flat_tree(n: usize) -> (TaffyTree<()>, NodeId) { + let mut taffy = TaffyTree::new(); + let mut children = Vec::with_capacity(n); + for _ in 0..n { + let child = taffy + .new_leaf(Style { + size: Size { width: Dimension::from_length(30.0), height: auto() }, + ..Default::default() + }) + .unwrap(); + children.push(child); + } + let root = taffy + .new_with_children( + Style { + display: Display::Flex, + size: Size { + width: Dimension::from_length(1000.0), + height: Dimension::from_length(500.0), + }, + ..Default::default() + }, + &children, + ) + .unwrap(); + (taffy, root) +} + +/// Build a flex tree with N children, where every 5th child is wrapped in a +/// Display::Contents node (simulating GestureDetector-like wrappers). +fn build_tree_with_contents(n: usize) -> (TaffyTree<()>, NodeId) { + let mut taffy = TaffyTree::new(); + let mut children = Vec::with_capacity(n); + for i in 0..n { + let leaf = taffy + .new_leaf(Style { + size: Size { width: Dimension::from_length(30.0), height: auto() }, + ..Default::default() + }) + .unwrap(); + if i % 5 == 0 { + // Wrap in contents node + let contents = taffy + .new_with_children( + Style { display: Display::Contents, ..Default::default() }, + &[leaf], + ) + .unwrap(); + children.push(contents); + } else { + children.push(leaf); + } + } + let root = taffy + .new_with_children( + Style { + display: Display::Flex, + size: Size { + width: Dimension::from_length(1000.0), + height: Dimension::from_length(500.0), + }, + ..Default::default() + }, + &children, + ) + .unwrap(); + (taffy, root) +} + +/// Build a deep tree (10 levels) with N leaves at the bottom, no contents. +fn build_deep_tree(depth: usize, breadth: usize) -> (TaffyTree<()>, NodeId) { + let mut taffy = TaffyTree::new(); + let root = build_deep_node(&mut taffy, depth, breadth); + (taffy, root) +} + +fn build_deep_node(taffy: &mut TaffyTree<()>, depth: usize, breadth: usize) -> NodeId { + if depth == 0 { + return taffy + .new_leaf(Style { + size: Size { width: Dimension::from_length(10.0), height: Dimension::from_length(10.0) }, + ..Default::default() + }) + .unwrap(); + } + let mut children = Vec::with_capacity(breadth); + for _ in 0..breadth { + children.push(build_deep_node(taffy, depth - 1, breadth)); + } + taffy + .new_with_children( + Style { display: Display::Flex, ..Default::default() }, + &children, + ) + .unwrap() +} + +fn bench_no_contents(c: &mut Criterion) { + let mut group = c.benchmark_group("display_contents/no_contents"); + for &n in &[10, 100, 1000] { + group.bench_with_input(BenchmarkId::new("flat", n), &n, |b, &n| { + b.iter_batched( + || build_flat_tree(n), + |(mut taffy, root)| { + taffy.compute_layout(root, Size::MAX_CONTENT).unwrap(); + }, + criterion::BatchSize::SmallInput, + ); + }); + } + // Deep tree: 4^6 = 4096 nodes + group.bench_function("deep_4x6", |b| { + b.iter_batched( + || build_deep_tree(6, 4), + |(mut taffy, root)| { + taffy.compute_layout(root, Size::MAX_CONTENT).unwrap(); + }, + criterion::BatchSize::SmallInput, + ); + }); + group.finish(); +} + +fn bench_with_contents(c: &mut Criterion) { + let mut group = c.benchmark_group("display_contents/with_contents"); + for &n in &[10, 100, 1000] { + group.bench_with_input(BenchmarkId::new("flat_20pct_contents", n), &n, |b, &n| { + b.iter_batched( + || build_tree_with_contents(n), + |(mut taffy, root)| { + taffy.compute_layout(root, Size::MAX_CONTENT).unwrap(); + }, + criterion::BatchSize::SmallInput, + ); + }); + } + group.finish(); +} + +fn bench_relayout(c: &mut Criterion) { + let mut group = c.benchmark_group("display_contents/relayout"); + // Measure cost of repeated layouts (resolved_children recomputed each time) + for &n in &[100, 1000] { + group.bench_with_input(BenchmarkId::new("no_contents", n), &n, |b, &n| { + let (mut taffy, root) = build_flat_tree(n); + taffy.compute_layout(root, Size::MAX_CONTENT).unwrap(); + b.iter(|| { + taffy.mark_dirty(root).unwrap(); + taffy.compute_layout(root, Size::MAX_CONTENT).unwrap(); + }); + }); + group.bench_with_input(BenchmarkId::new("with_contents", n), &n, |b, &n| { + let (mut taffy, root) = build_tree_with_contents(n); + taffy.compute_layout(root, Size::MAX_CONTENT).unwrap(); + b.iter(|| { + taffy.mark_dirty(root).unwrap(); + taffy.compute_layout(root, Size::MAX_CONTENT).unwrap(); + }); + }); + } + group.finish(); +} + +criterion_group!(benches, bench_no_contents, bench_with_contents, bench_relayout); +criterion_main!(benches); diff --git a/src/style/mod.rs b/src/style/mod.rs index e32018624..b69686e3e 100644 --- a/src/style/mod.rs +++ b/src/style/mod.rs @@ -58,16 +58,16 @@ pub trait CheapCloneStr: AsRef + for<'a> From<&'a str> + From + PartialEq + Eq + Clone + Default + Debug + 'static { } -#[cfg(any(feature = "alloc", feature = "std"))] -impl CheapCloneStr for T where - T: AsRef + for<'a> From<&'a str> + From + PartialEq + Eq + Clone + Default + Debug + 'static -{ -} /// Trait that represents a cheaply clonable string. If you're unsure what to use here /// consider `Arc` or `string_cache::Atom`. #[cfg(not(any(feature = "alloc", feature = "std")))] pub trait CheapCloneStr {} +#[cfg(any(feature = "alloc", feature = "std"))] +impl CheapCloneStr for T where + T: AsRef + for<'a> From<&'a str> + From + PartialEq + Eq + Clone + Default + Debug + 'static +{ +} #[cfg(not(any(feature = "alloc", feature = "std")))] impl CheapCloneStr for T {} @@ -188,6 +188,10 @@ pub enum Display { /// The children will follow the CSS Grid layout algorithm #[cfg(feature = "grid")] Grid, + /// The node is invisible to layout: it generates no box, and its children + /// are laid out as if they were direct children of this node's parent. + /// Equivalent to CSS `display: contents`. + Contents, /// The node is hidden, and it's children will also be hidden None, } @@ -219,6 +223,7 @@ impl Default for Display { #[cfg(feature = "parse")] crate::util::parse::impl_parse_for_keyword_enum!(Display, "none" => None, + "contents" => Contents, #[cfg(feature = "flexbox")] "flex" => Flex, #[cfg(feature = "grid")] @@ -231,6 +236,7 @@ impl core::fmt::Display for Display { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { match self { Display::None => write!(f, "NONE"), + Display::Contents => write!(f, "CONTENTS"), #[cfg(feature = "block_layout")] Display::Block => write!(f, "BLOCK"), #[cfg(feature = "flexbox")] @@ -670,7 +676,7 @@ impl CoreStyle for Style { #[inline(always)] fn box_generation_mode(&self) -> BoxGenerationMode { match self.display { - Display::None => BoxGenerationMode::None, + Display::None | Display::Contents => BoxGenerationMode::None, _ => BoxGenerationMode::Normal, } } diff --git a/src/tree/taffy_tree.rs b/src/tree/taffy_tree.rs index ab9beb038..9fb5eee89 100644 --- a/src/tree/taffy_tree.rs +++ b/src/tree/taffy_tree.rs @@ -161,6 +161,18 @@ pub struct TaffyTree { /// The indexes in the outer vector correspond to the position of the child [`NodeData`] parents: SlotMap>, + /// Resolved (flattened) children for nodes that have `Display::Contents` + /// children. Contents nodes are replaced by their own children (recursively). + /// Only populated for nodes that actually need resolution. + /// + /// Recomputed before each layout pass via `resolve_contents_children()`. + resolved_children: SecondaryMap>, + + /// Set when a mutation could affect contents resolution. Cleared after resolve. + /// If the resolve walk finds no contents nodes, it clears this flag so future + /// layouts skip the walk entirely. + contents_dirty: bool, + /// Layout mode configuration config: TaffyConfig, } @@ -183,6 +195,12 @@ impl Iterator for TaffyTreeChildIter<'_> { } // TraversePartialTree impl for TaffyTree +// +// `child_ids`, `child_count`, and `get_child_id` all resolve through +// `Display::Contents` nodes: if a direct child has `Display::Contents`, +// it is replaced by its own children (recursively). This implements CSS +// `display: contents` semantics where the node generates no box and its +// children are promoted to the parent's child list. impl TraversePartialTree for TaffyTree { type ChildIter<'a> = TaffyTreeChildIter<'a> @@ -191,17 +209,29 @@ impl TraversePartialTree for TaffyTree { #[inline(always)] fn child_ids(&self, parent_node_id: NodeId) -> Self::ChildIter<'_> { - TaffyTreeChildIter(self.children[parent_node_id.into()].iter()) + let key: DefaultKey = parent_node_id.into(); + match self.resolved_children.get(key) { + Some(resolved) => TaffyTreeChildIter(resolved.iter()), + None => TaffyTreeChildIter(self.children[key].iter()), + } } #[inline(always)] fn child_count(&self, parent_node_id: NodeId) -> usize { - self.children[parent_node_id.into()].len() + let key: DefaultKey = parent_node_id.into(); + match self.resolved_children.get(key) { + Some(resolved) => resolved.len(), + None => self.children[key].len(), + } } #[inline(always)] fn get_child_id(&self, parent_node_id: NodeId, id: usize) -> NodeId { - self.children[parent_node_id.into()][id] + let key: DefaultKey = parent_node_id.into(); + match self.resolved_children.get(key) { + Some(resolved) => resolved[id], + None => self.children[key][id], + } } } @@ -233,6 +263,7 @@ impl PrintTree for TaffyTree { match (num_children, display) { (_, Display::None) => "NONE", + (_, Display::Contents) => "CONTENTS", (0, _) => "LEAF", #[cfg(feature = "block_layout")] (_, Display::Block) => "BLOCK", @@ -273,6 +304,7 @@ where pub(crate) measure_function: MeasureFunction, } + impl TaffyView<'_, NodeContext, MeasureFunction> where MeasureFunction: @@ -309,6 +341,13 @@ where // Dispatch to a layout algorithm based on the node's display style and whether the node has children or not. match (display_mode, has_children) { (Display::None, _) => compute_hidden_layout(tree, node_id), + // Contents nodes generate no box. Zero layout, but children are NOT hidden — + // they are promoted to this node's parent by TraversePartialTree::child_ids. + (Display::Contents, _) => { + tree.set_unrounded_layout(node_id, &Layout::with_order(0)); + tree.cache_clear(node_id); + LayoutOutput::HIDDEN + } #[cfg(feature = "block_layout")] (Display::Block, true) => compute_block_layout(tree, node_id, inputs, block_ctx), #[cfg(feature = "flexbox")] @@ -552,6 +591,8 @@ impl TaffyTree { children: SlotMap::with_capacity(capacity), parents: SlotMap::with_capacity(capacity), node_context_data: SecondaryMap::with_capacity(capacity), + resolved_children: SecondaryMap::new(), + contents_dirty: false, config: TaffyConfig::default(), } } @@ -568,6 +609,7 @@ impl TaffyTree { /// Creates and adds a new unattached leaf node to the tree, and returns the node of the new node pub fn new_leaf(&mut self, layout: Style) -> TaffyResult { + if layout.display == Display::Contents { self.contents_dirty = true; } let id = self.nodes.insert(NodeData::new(layout)); let _ = self.children.insert(new_vec_with_capacity(0)); let _ = self.parents.insert(None); @@ -579,6 +621,7 @@ impl TaffyTree { /// /// Creates and adds a new leaf node with a supplied context pub fn new_leaf_with_context(&mut self, layout: Style, context: NodeContext) -> TaffyResult { + if layout.display == Display::Contents { self.contents_dirty = true; } let mut data = NodeData::new(layout); data.has_context = true; @@ -593,6 +636,7 @@ impl TaffyTree { /// Creates and adds a new node, which may have any number of `children` pub fn new_with_children(&mut self, layout: Style, children: &[NodeId]) -> TaffyResult { + if layout.display == Display::Contents { self.contents_dirty = true; } let id = NodeId::from(self.nodes.insert(NodeData::new(layout))); for child in children { @@ -610,6 +654,8 @@ impl TaffyTree { self.nodes.clear(); self.children.clear(); self.parents.clear(); + self.resolved_children.clear(); + self.contents_dirty = false; } /// Remove a specific node from the tree and drop it @@ -617,6 +663,7 @@ impl TaffyTree { /// Returns the id of the node removed. pub fn remove(&mut self, node: NodeId) -> TaffyResult { let key = node.into(); + if self.nodes[key].style.display == Display::Contents { self.contents_dirty = true; } if let Some(parent) = self.parents[key] { if let Some(children) = self.children.get_mut(parent.into()) { children.retain(|f| *f != node); @@ -832,7 +879,13 @@ impl TaffyTree { /// Sets the [`Style`] of the provided `node` #[inline] pub fn set_style(&mut self, node: NodeId, style: Style) -> TaffyResult<()> { - self.nodes[node.into()].style = style; + let key = node.into(); + let old_display = self.nodes[key].style.display; + let new_display = style.display; + if old_display == Display::Contents || new_display == Display::Contents { + self.contents_dirty = true; + } + self.nodes[key].style = style; self.mark_dirty(node)?; Ok(()) } @@ -901,6 +954,69 @@ impl TaffyTree { Ok(self.nodes[node.into()].cache.is_empty()) } + /// Resolve `Display::Contents` children for the entire tree rooted at `root`. + /// + /// For each node whose direct children include any `Display::Contents` node, + /// pre-computes a flattened child list where contents nodes are replaced by + /// their children (recursively). Stored in `resolved_children` so that + /// `child_ids()` returns the flattened list with zero allocation. + fn resolve_contents_children(&mut self, root: NodeId) { + self.resolved_children.clear(); + + // Fast path: no contents-related mutations since last resolve. + if !self.contents_dirty { + return; + } + + // Walk all nodes reachable from root. We collect into a Vec first to + // avoid borrowing conflicts (iterating nodes while reading children). + let all_nodes: Vec = { + let mut stack = vec![root]; + let mut result = Vec::new(); + while let Some(id) = stack.pop() { + result.push(id); + for &child in self.children[id.into()].iter() { + stack.push(child); + } + } + result + }; + + let mut found_any = false; + for node_id in all_nodes { + let key: DefaultKey = node_id.into(); + let children = &self.children[key]; + + let has_contents_child = children.iter().any(|child| { + self.nodes[(*child).into()].style.display == Display::Contents + }); + + if has_contents_child { + found_any = true; + let mut resolved = ChildrenVec::new(); + self.collect_resolved_children(node_id, &mut resolved); + self.resolved_children.insert(key, resolved); + } + } + + // If no contents nodes were found, clear the flag so future layouts + // skip the walk entirely (e.g. after all contents nodes are removed). + self.contents_dirty = found_any; + } + + /// Recursively collect the resolved children for a node, flattening through + /// `Display::Contents` nodes. + fn collect_resolved_children(&self, node_id: NodeId, out: &mut ChildrenVec) { + for &child in self.children[node_id.into()].iter() { + if self.nodes[child.into()].style.display == Display::Contents { + // Recurse: replace contents node with its children + self.collect_resolved_children(child, out); + } else { + out.push(child); + } + } + } + /// Updates the stored layout of the provided `node` and its children pub fn compute_layout_with_measure( &mut self, @@ -912,6 +1028,10 @@ impl TaffyTree { MeasureFunction: FnMut(Size>, Size, NodeId, Option<&mut NodeContext>, &Style) -> Size, { + // Resolve Display::Contents children before layout so child_ids() + // returns flattened lists during the entire layout pass. + self.resolve_contents_children(node_id); + let use_rounding = self.config.use_rounding; let mut taffy_view = TaffyView { taffy: self, measure_function }; compute_root_layout(&mut taffy_view, node_id, available_space); diff --git a/test_fixtures/contents/contents_flex_basic.html b/test_fixtures/contents/contents_flex_basic.html new file mode 100644 index 000000000..eee9e648e --- /dev/null +++ b/test_fixtures/contents/contents_flex_basic.html @@ -0,0 +1,23 @@ + + + + + + + Test description + + + + +
+
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/test_fixtures/contents/contents_flex_deeply_nested.html b/test_fixtures/contents/contents_flex_deeply_nested.html new file mode 100644 index 000000000..86d4afcee --- /dev/null +++ b/test_fixtures/contents/contents_flex_deeply_nested.html @@ -0,0 +1,28 @@ + + + + + + + Test description + + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/test_fixtures/contents/contents_flex_mixed_with_none.html b/test_fixtures/contents/contents_flex_mixed_with_none.html new file mode 100644 index 000000000..48415deca --- /dev/null +++ b/test_fixtures/contents/contents_flex_mixed_with_none.html @@ -0,0 +1,24 @@ + + + + + + + Test description + + + + + +
+
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/test_fixtures/contents/contents_flex_nested.html b/test_fixtures/contents/contents_flex_nested.html new file mode 100644 index 000000000..de2898722 --- /dev/null +++ b/test_fixtures/contents/contents_flex_nested.html @@ -0,0 +1,26 @@ + + + + + + + Test description + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/test_fixtures/contents/contents_flex_single_child.html b/test_fixtures/contents/contents_flex_single_child.html new file mode 100644 index 000000000..c18209e27 --- /dev/null +++ b/test_fixtures/contents/contents_flex_single_child.html @@ -0,0 +1,21 @@ + + + + + + + Test description + + + + + +
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/test_fixtures/contents/contents_flex_with_flex_child.html b/test_fixtures/contents/contents_flex_with_flex_child.html new file mode 100644 index 000000000..4236c240a --- /dev/null +++ b/test_fixtures/contents/contents_flex_with_flex_child.html @@ -0,0 +1,22 @@ + + + + + + + Test description + + + + + +
+
+
+
+
+
+
+ + + \ No newline at end of file diff --git a/tests/contents.rs b/tests/contents.rs new file mode 100644 index 000000000..2debff373 --- /dev/null +++ b/tests/contents.rs @@ -0,0 +1,214 @@ +use taffy::prelude::*; + +/// Basic: contents node's children are promoted and laid out by the parent flex container. +/// Equivalent to PR #534's contents_flex_basic test. +/// +/// Layout: flex row, 400x300, justify-content: space-between +/// [30px] [30px] [30px] [contents: [30px] [30px]] +/// +/// Should lay out as 5 equal-width items spaced across 400px. +#[test] +fn contents_flex_basic() { + let mut taffy = TaffyTree::<()>::new(); + + let node0 = taffy.new_leaf(Style { + size: Size { width: Dimension::from_length(30.0), height: auto() }, + ..Default::default() + }).unwrap(); + let node1 = taffy.new_leaf(Style { + size: Size { width: Dimension::from_length(30.0), height: auto() }, + ..Default::default() + }).unwrap(); + let node2 = taffy.new_leaf(Style { + size: Size { width: Dimension::from_length(30.0), height: auto() }, + ..Default::default() + }).unwrap(); + + let node30 = taffy.new_leaf(Style { + size: Size { width: Dimension::from_length(30.0), height: auto() }, + ..Default::default() + }).unwrap(); + let node31 = taffy.new_leaf(Style { + size: Size { width: Dimension::from_length(30.0), height: auto() }, + ..Default::default() + }).unwrap(); + + let contents = taffy.new_with_children( + Style { display: Display::Contents, ..Default::default() }, + &[node30, node31], + ).unwrap(); + + let root = taffy.new_with_children( + Style { + display: Display::Flex, + justify_content: Some(JustifyContent::SpaceBetween), + size: Size { + width: Dimension::from_length(400.0), + height: Dimension::from_length(300.0), + }, + ..Default::default() + }, + &[node0, node1, node2, contents], + ).unwrap(); + + taffy.compute_layout(root, Size::MAX_CONTENT).unwrap(); + + // Contents node itself has zero layout + let cl = taffy.layout(contents).unwrap(); + assert_eq!(cl.size.width, 0.0); + assert_eq!(cl.size.height, 0.0); + + // 5 items, 5 * 30 = 150px used, 250px remaining, 4 gaps = 62.5px each + // Positions: 0, 92.5, 185, 277.5, 370 + let l0 = taffy.layout(node0).unwrap(); + assert_eq!(l0.size.width, 30.0); + assert_eq!(l0.location.x, 0.0); + + let l1 = taffy.layout(node1).unwrap(); + assert_eq!(l1.size.width, 30.0); + + let l30 = taffy.layout(node30).unwrap(); + assert_eq!(l30.size.width, 30.0); + assert_eq!(l30.size.height, 300.0, "promoted child should stretch to parent height"); + + let l31 = taffy.layout(node31).unwrap(); + assert_eq!(l31.size.width, 30.0); + assert_eq!(l31.location.x, 370.0, "last promoted child at far right"); +} + +/// Nested: contents within contents, children promoted recursively. +#[test] +fn contents_flex_nested() { + let mut taffy = TaffyTree::<()>::new(); + + let leaf1 = taffy.new_leaf(Style { + size: Size { width: Dimension::from_length(50.0), height: auto() }, + ..Default::default() + }).unwrap(); + let leaf2 = taffy.new_leaf(Style { + size: Size { width: Dimension::from_length(50.0), height: auto() }, + ..Default::default() + }).unwrap(); + + let inner_contents = taffy.new_with_children( + Style { display: Display::Contents, ..Default::default() }, + &[leaf2], + ).unwrap(); + + let outer_contents = taffy.new_with_children( + Style { display: Display::Contents, ..Default::default() }, + &[leaf1, inner_contents], + ).unwrap(); + + let root = taffy.new_with_children( + Style { + display: Display::Flex, + size: Size { + width: Dimension::from_length(200.0), + height: Dimension::from_length(100.0), + }, + ..Default::default() + }, + &[outer_contents], + ).unwrap(); + + taffy.compute_layout(root, Size::MAX_CONTENT).unwrap(); + + // Both leaves should be laid out as direct children of root + let l1 = taffy.layout(leaf1).unwrap(); + assert_eq!(l1.size.width, 50.0); + assert_eq!(l1.size.height, 100.0); // stretch + assert_eq!(l1.location.x, 0.0); + + let l2 = taffy.layout(leaf2).unwrap(); + assert_eq!(l2.size.width, 50.0); + assert_eq!(l2.location.x, 50.0); + + // Both contents nodes have zero layout + assert_eq!(taffy.layout(outer_contents).unwrap().size.width, 0.0); + assert_eq!(taffy.layout(inner_contents).unwrap().size.width, 0.0); +} + +/// Contents children are NOT hidden (unlike Display::None) +#[test] +fn contents_children_not_hidden() { + let mut taffy = TaffyTree::<()>::new(); + + let contents_child = taffy.new_leaf(Style { + size: Size { width: Dimension::from_length(50.0), height: Dimension::from_length(50.0) }, + ..Default::default() + }).unwrap(); + let none_child = taffy.new_leaf(Style { + size: Size { width: Dimension::from_length(50.0), height: Dimension::from_length(50.0) }, + ..Default::default() + }).unwrap(); + + let contents = taffy.new_with_children( + Style { display: Display::Contents, ..Default::default() }, + &[contents_child], + ).unwrap(); + let none = taffy.new_with_children( + Style { display: Display::None, ..Default::default() }, + &[none_child], + ).unwrap(); + + let root = taffy.new_with_children( + Style { + display: Display::Flex, + size: Size { width: Dimension::from_length(200.0), height: Dimension::from_length(200.0) }, + ..Default::default() + }, + &[contents, none], + ).unwrap(); + + taffy.compute_layout(root, Size::MAX_CONTENT).unwrap(); + + // Contents child is promoted and laid out (has explicit 50x50 size) + let cl = taffy.layout(contents_child).unwrap(); + assert_eq!(cl.size.width, 50.0); + assert_eq!(cl.size.height, 50.0); + + // None child is hidden + let nl = taffy.layout(none_child).unwrap(); + assert_eq!(nl.size.width, 0.0); + assert_eq!(nl.size.height, 0.0); +} + +/// Style roundtrip +#[test] +fn contents_style_roundtrip() { + let mut taffy = TaffyTree::<()>::new(); + let node = taffy.new_leaf(Style { display: Display::Contents, ..Default::default() }).unwrap(); + assert_eq!(taffy.style(node).unwrap().display, Display::Contents); + + taffy.set_style(node, Style { display: Display::None, ..Default::default() }).unwrap(); + assert_eq!(taffy.style(node).unwrap().display, Display::None); + + taffy.set_style(node, Style { display: Display::Contents, ..Default::default() }).unwrap(); + assert_eq!(taffy.style(node).unwrap().display, Display::Contents); +} + +/// No overhead when no contents nodes exist +#[test] +fn no_contents_no_overhead() { + let mut taffy = TaffyTree::<()>::new(); + + let child = taffy.new_leaf(Style { + size: Size { width: Dimension::from_length(50.0), height: Dimension::from_length(50.0) }, + ..Default::default() + }).unwrap(); + let root = taffy.new_with_children( + Style { + display: Display::Flex, + size: Size { width: Dimension::from_length(200.0), height: Dimension::from_length(200.0) }, + ..Default::default() + }, + &[child], + ).unwrap(); + + // This should not walk the tree (contents_count == 0) + taffy.compute_layout(root, Size::MAX_CONTENT).unwrap(); + + let cl = taffy.layout(child).unwrap(); + assert_eq!(cl.size.width, 50.0); +} diff --git a/tests/xml/contents/contents_flex_basic__border_box_ltr.xml b/tests/xml/contents/contents_flex_basic__border_box_ltr.xml new file mode 100644 index 000000000..c6a31b87c --- /dev/null +++ b/tests/xml/contents/contents_flex_basic__border_box_ltr.xml @@ -0,0 +1,25 @@ + + + +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + diff --git a/tests/xml/contents/contents_flex_deeply_nested__border_box_ltr.xml b/tests/xml/contents/contents_flex_deeply_nested__border_box_ltr.xml new file mode 100644 index 000000000..4abf2f3f7 --- /dev/null +++ b/tests/xml/contents/contents_flex_deeply_nested__border_box_ltr.xml @@ -0,0 +1,35 @@ + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/tests/xml/contents/contents_flex_mixed_with_none__border_box_ltr.xml b/tests/xml/contents/contents_flex_mixed_with_none__border_box_ltr.xml new file mode 100644 index 000000000..f61c088cc --- /dev/null +++ b/tests/xml/contents/contents_flex_mixed_with_none__border_box_ltr.xml @@ -0,0 +1,25 @@ + + + +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + diff --git a/tests/xml/contents/contents_flex_nested__border_box_ltr.xml b/tests/xml/contents/contents_flex_nested__border_box_ltr.xml new file mode 100644 index 000000000..fc68e1940 --- /dev/null +++ b/tests/xml/contents/contents_flex_nested__border_box_ltr.xml @@ -0,0 +1,31 @@ + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + diff --git a/tests/xml/contents/contents_flex_single_child__border_box_ltr.xml b/tests/xml/contents/contents_flex_single_child__border_box_ltr.xml new file mode 100644 index 000000000..71b38899d --- /dev/null +++ b/tests/xml/contents/contents_flex_single_child__border_box_ltr.xml @@ -0,0 +1,19 @@ + + + +
+
+
+
+
+
+ + + + + + + + + + diff --git a/tests/xml/contents/contents_flex_with_flex_child__border_box_ltr.xml b/tests/xml/contents/contents_flex_with_flex_child__border_box_ltr.xml new file mode 100644 index 000000000..20f64ef16 --- /dev/null +++ b/tests/xml/contents/contents_flex_with_flex_child__border_box_ltr.xml @@ -0,0 +1,21 @@ + + + +
+
+
+
+
+
+
+ + + + + + + + + + + diff --git a/tests/xml/mod.rs b/tests/xml/mod.rs index 7218ac1f7..ad75e1198 100644 --- a/tests/xml/mod.rs +++ b/tests/xml/mod.rs @@ -22913,3 +22913,34 @@ mod leaf { crate::run_xml_test("leaf", "leaf_with_content_and_padding_border__content_box_rtl"); } } +mod contents { + #[test] + fn contents_flex_basic__border_box_ltr() { + crate::run_xml_test("contents", "contents_flex_basic__border_box_ltr"); + } + + #[test] + fn contents_flex_nested__border_box_ltr() { + crate::run_xml_test("contents", "contents_flex_nested__border_box_ltr"); + } + + #[test] + fn contents_flex_single_child__border_box_ltr() { + crate::run_xml_test("contents", "contents_flex_single_child__border_box_ltr"); + } + + #[test] + fn contents_flex_with_flex_child__border_box_ltr() { + crate::run_xml_test("contents", "contents_flex_with_flex_child__border_box_ltr"); + } + + #[test] + fn contents_flex_mixed_with_none__border_box_ltr() { + crate::run_xml_test("contents", "contents_flex_mixed_with_none__border_box_ltr"); + } + + #[test] + fn contents_flex_deeply_nested__border_box_ltr() { + crate::run_xml_test("contents", "contents_flex_deeply_nested__border_box_ltr"); + } +}