use crate::node::{ImageData, NodeData, SpecialElementData};
use crate::{document::BaseDocument, dom_node_id, node::Node, taffy_node_id};
use markup5ever::local_name;
use std::cell::Ref;
use std::sync::Arc;
use style::Atom;
use style::values::computed::CSSPixelLength;
use style::values::computed::length_percentage::CalcLengthPercentage;
use taffy::{
BlockContext, CollapsibleMarginSet, FlexDirection, LayoutPartialTree, MaybeResolve, NodeId,
ResolveOrZero, RoundTree, Style, TraversePartialTree, TraverseTree, compute_block_layout,
compute_cached_layout, compute_flexbox_layout, compute_grid_layout, compute_leaf_layout,
prelude::*,
};
#[cfg(not(target_arch = "wasm32"))]
pub(crate) mod layout_panic_probe {
use std::cell::RefCell;
use std::sync::OnceLock;
thread_local! {
static IN_FLIGHT: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
static INNERMOST: std::cell::Cell<Option<blitz_traits::node_id::NodeId>> =
const { std::cell::Cell::new(None) };
}
pub(crate) fn enabled() -> bool {
static ENABLED: OnceLock<bool> = OnceLock::new();
*ENABLED.get_or_init(|| {
let on = std::env::var_os("BLITZ_TRACE_LAYOUT_PANIC").is_some();
if on {
install_hook();
}
on
})
}
fn install_hook() {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
IN_FLIGHT.with(|stack| {
let stack = stack.borrow();
if stack.is_empty() {
eprintln!("[blitz-layout-panic] no layout in flight on this thread");
} else {
eprintln!("[blitz-layout-panic] innermost first:");
for entry in stack.iter().rev().take(12) {
eprintln!("[blitz-layout-panic] {entry}");
}
eprintln!("[blitz-layout-panic] ({} deep)", stack.len());
}
});
previous(info);
}));
}
const RUNAWAY_DEPTH: usize = 512;
pub(crate) fn innermost_node() -> Option<blitz_traits::node_id::NodeId> {
INNERMOST.with(std::cell::Cell::get)
}
pub(crate) fn push(node_id: blitz_traits::node_id::NodeId, description: String) {
INNERMOST.with(|cell| cell.set(Some(node_id)));
IN_FLIGHT.with(|stack| {
let mut stack = stack.borrow_mut();
stack.push(description);
if stack.len() == RUNAWAY_DEPTH {
eprintln!(
"[blitz-layout-panic] runaway: {RUNAWAY_DEPTH} nested layouts, innermost first:"
);
for entry in stack.iter().rev().take(24) {
eprintln!("[blitz-layout-panic] {entry}");
}
}
});
}
pub(crate) fn pop() {
IN_FLIGHT.with(|stack| {
stack.borrow_mut().pop();
});
}
}
#[cfg(feature = "log-phase-times")]
pub mod layout_counters {
use blitz_traits::node_id::NodeId;
use std::cell::Cell;
thread_local! {
static ACTIVE: Cell<bool> = const { Cell::new(false) };
static COMPUTED: Cell<u64> = const { Cell::new(0) };
static CACHES_CLEARED: Cell<u64> = const { Cell::new(0) };
static LOOKUPS: Cell<u64> = const { Cell::new(0) };
static HITS: Cell<u64> = const { Cell::new(0) };
static DISTINCT: std::cell::RefCell<std::collections::HashMap<NodeId, u32>> =
std::cell::RefCell::new(std::collections::HashMap::new());
}
pub(crate) fn begin(active: bool) {
ACTIVE.with(|enabled| enabled.set(active));
if !active {
return;
}
COMPUTED.with(|count| count.set(0));
CACHES_CLEARED.with(|count| count.set(0));
LOOKUPS.with(|count| count.set(0));
HITS.with(|count| count.set(0));
DISTINCT.with(|seen| seen.borrow_mut().clear());
}
#[inline(always)]
fn active() -> bool {
ACTIVE.with(Cell::get)
}
pub(crate) fn note_computed(node_id: NodeId) {
if !active() {
return;
}
COMPUTED.with(|count| count.set(count.get() + 1));
DISTINCT.with(|seen| {
*seen.borrow_mut().entry(node_id).or_insert(0u32) += 1;
});
}
pub(crate) fn worst_offenders(limit: usize) -> Vec<(NodeId, u32)> {
DISTINCT.with(|seen| {
let mut rows: Vec<(NodeId, u32)> = seen
.borrow()
.iter()
.map(|(id, count)| (*id, *count))
.collect();
rows.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
rows.truncate(limit);
rows
})
}
pub(crate) fn note_cache_cleared() {
if !active() {
return;
}
CACHES_CLEARED.with(|count| count.set(count.get() + 1));
}
pub(crate) fn note_lookup(hit: bool) {
if !active() {
return;
}
LOOKUPS.with(|count| count.set(count.get() + 1));
if hit {
HITS.with(|count| count.set(count.get() + 1));
}
}
#[derive(Clone, Copy)]
pub struct LayoutCounts {
pub computed: u64,
pub distinct: usize,
pub caches_cleared: u64,
pub lookups: u64,
pub hits: u64,
}
impl LayoutCounts {
const ZERO: Self = Self {
computed: 0,
distinct: 0,
caches_cleared: 0,
lookups: 0,
hits: 0,
};
}
thread_local! {
static LAST: Cell<LayoutCounts> = const { Cell::new(LayoutCounts::ZERO) };
}
#[must_use]
pub fn last() -> LayoutCounts {
LAST.with(Cell::get)
}
pub fn take() -> LayoutCounts {
if !active() {
LAST.with(|last| last.set(LayoutCounts::ZERO));
return LayoutCounts::ZERO;
}
let counts = LayoutCounts {
computed: COMPUTED.with(|count| count.replace(0)),
distinct: DISTINCT.with(|seen| {
let mut seen = seen.borrow_mut();
let len = seen.len();
seen.clear();
len
}),
caches_cleared: CACHES_CLEARED.with(|count| count.replace(0)),
lookups: LOOKUPS.with(|count| count.replace(0)),
hits: HITS.with(|count| count.replace(0)),
};
ACTIVE.with(|active| active.set(false));
LAST.with(|last| last.set(counts));
counts
}
}
pub(crate) mod construct;
pub(crate) mod damage;
pub(crate) mod inline;
pub(crate) mod list;
pub(crate) mod replaced;
pub(crate) mod table;
use self::replaced::{ReplacedContext, is_replaced_element, replaced_measure_function};
use self::table::TableTreeWrapper;
pub(crate) fn resolve_calc_value(calc_ptr: *const (), parent_size: f32) -> f32 {
let calc = unsafe { &*(calc_ptr as *const CalcLengthPercentage) };
let result = calc.resolve(CSSPixelLength::new(parent_size));
result.px()
}
impl BaseDocument {
fn node_from_id(&self, node_id: taffy::prelude::NodeId) -> &Node {
&self.nodes[dom_node_id(node_id)]
}
fn node_from_id_mut(&mut self, node_id: taffy::prelude::NodeId) -> &mut Node {
&mut self.nodes[dom_node_id(node_id)]
}
#[cfg(not(target_arch = "wasm32"))]
fn describe_node_for_panic(
&self,
node_id: blitz_traits::node_id::NodeId,
inputs: &taffy::LayoutInput,
) -> String {
let Some(node) = self.nodes.get(node_id) else {
return format!("node {node_id} (gone)");
};
let Some(element) = node.data.downcast_element() else {
return format!("node {node_id} <{:?}>", node.data.kind());
};
let attr = |name: &str| -> Option<&str> {
element
.attrs
.iter()
.find(|a| a.name.local.as_ref() == name)
.map(|a| a.value.as_ref())
};
format!(
"node {node_id} <{}{}{}> known={:?}x{:?} avail={:?}x{:?} mode={:?}/{:?}",
element.name.local,
attr("id").map(|v| format!(" id={v}")).unwrap_or_default(),
attr("class")
.map(|v| format!(" class=\"{}\"", &v[..v.len().min(160)]))
.unwrap_or_default(),
inputs.known_dimensions.width,
inputs.known_dimensions.height,
inputs.available_space.width,
inputs.available_space.height,
inputs.run_mode,
inputs.axis,
)
}
}
fn select_metrics_of(
doc: &BaseDocument,
node_id: blitz_traits::node_id::NodeId,
) -> Option<(usize, f32)> {
let node = doc.nodes.get(node_id)?;
let element = node.data.downcast_element()?;
if element.name.local != local_name!("select") {
return None;
}
let widest = doc
.select_options(node_id)
.into_iter()
.map(|option_id| doc.option_label(option_id).chars().count())
.max()
.unwrap_or(0);
let rows = element
.attr(local_name!("size"))
.and_then(|size| size.parse::<f32>().ok())
.filter(|rows| *rows >= 1.0)
.unwrap_or(if element.attr(local_name!("multiple")).is_some() {
4.0
} else {
1.0
});
Some((widest, rows))
}
impl BaseDocument {
fn select_metrics(&self, node_id: blitz_traits::node_id::NodeId) -> Option<(usize, f32)> {
select_metrics_of(self, node_id)
}
fn compute_child_layout_internal(
&mut self,
node_id: NodeId,
inputs: taffy::tree::LayoutInput,
block_ctx: Option<&mut BlockContext<'_>>,
) -> taffy::tree::LayoutOutput {
#[cfg(feature = "log-phase-times")]
layout_counters::note_computed(dom_node_id(node_id));
let select_metrics = self.select_metrics(dom_node_id(node_id));
let node = &mut self.nodes[dom_node_id(node_id)];
let font_styles = node.primary_styles().map(|style| {
use style::values::computed::font::LineHeight;
let font_size = style.clone_font_size().used_size().px();
let line_height = match style.clone_line_height() {
LineHeight::Normal => font_size * 1.2,
LineHeight::Number(num) => font_size * num.0,
LineHeight::Length(value) => value.0.px(),
};
(font_size, line_height)
});
let font_size = font_styles.map(|s| s.0);
let resolved_line_height = font_styles.map(|s| s.1);
match &mut node.data {
NodeData::Text(data) => {
#[cfg(feature = "tracing")]
tracing::error!(
node_id = ?dom_node_id(node_id),
data = ?data,
"Tried to lay out text node individually",
);
#[cfg(not(feature = "tracing"))]
let _ = data;
taffy::LayoutOutput::HIDDEN
}
NodeData::Element(element_data) | NodeData::AnonymousBlock(element_data) => {
if let Some((widest_label, rows)) = select_metrics {
let advance = font_size.unwrap_or(16.0) * 0.6;
let line_height = resolved_line_height.unwrap_or(16.0);
return compute_leaf_layout(
inputs,
node.style(),
resolve_calc_value,
|_known_size, _available_space| taffy::Size {
width: widest_label as f32 * advance,
height: line_height * rows,
},
);
}
if *element_data.name.local == *"textarea" {
let rows = element_data
.attr(local_name!("rows"))
.and_then(|val| val.parse::<f32>().ok())
.unwrap_or(2.0);
let cols = element_data
.attr(local_name!("cols"))
.and_then(|val| val.parse::<f32>().ok());
let intrinsic_height = resolved_line_height.unwrap_or(16.0) * rows;
let content_width = node
.style()
.size
.width
.maybe_resolve(inputs.parent_size.width, resolve_calc_value)
.or(inputs.known_dimensions.width)
.or(match inputs.available_space.width {
taffy::AvailableSpace::Definite(width) => Some(width),
_ => None,
})
.map(|width| {
let inset = node
.style()
.padding
.resolve_or_zero(inputs.parent_size, resolve_calc_value)
.horizontal_components()
.sum()
+ node
.style()
.border
.resolve_or_zero(inputs.parent_size, resolve_calc_value)
.horizontal_components()
.sum();
(width - inset).max(0.0)
});
let mut content_height = intrinsic_height;
if let Some(width) = content_width.filter(|width| *width > 0.0) {
let font_ctx = self.font_ctx.clone();
let layout_ctx = &mut self.layout_ctx;
let node = &mut self.nodes[dom_node_id(node_id)];
if let Some(input) = node
.data
.downcast_element_mut()
.and_then(|el| el.text_input_data_mut())
{
input.sync_multiline_width(
&mut font_ctx.lock().unwrap(),
layout_ctx,
width,
);
if let Some(layout) = input.editor.try_layout() {
content_height = content_height.max(layout.height());
}
}
}
let node = &mut self.nodes[dom_node_id(node_id)];
let mut output = compute_leaf_layout(
inputs,
node.style(),
resolve_calc_value,
|_known_size, _available_space| taffy::Size {
width: cols
.map(|cols| cols * font_size.unwrap_or(16.0) * 0.6)
.unwrap_or(300.0),
height: intrinsic_height,
},
);
output.content_size.height = output.content_size.height.max(content_height);
output.content_size.width = output.content_size.width.max(output.size.width);
return output;
}
if *element_data.name.local == *"input" {
match element_data.attr(local_name!("type")) {
Some("hidden") => {
node.style_mut().display = Display::None;
return taffy::LayoutOutput::HIDDEN;
}
Some("checkbox") => {
return compute_leaf_layout(
inputs,
node.style(),
resolve_calc_value,
|_known_size, _available_space| {
let width = node.style().size.width.resolve_or_zero(
inputs.parent_size.width,
resolve_calc_value,
);
let height = node.style().size.height.resolve_or_zero(
inputs.parent_size.height,
resolve_calc_value,
);
let min_size = width.min(height);
taffy::Size {
width: min_size,
height: min_size,
}
},
);
}
None
| Some(
"text" | "password" | "email" | "number" | "tel" | "url" | "search",
) => {
return compute_leaf_layout(
inputs,
node.style(),
resolve_calc_value,
|_known_size, _available_space| taffy::Size {
width: match inputs.available_space.width {
AvailableSpace::Definite(limit) => limit.min(300.0),
AvailableSpace::MinContent => 0.0,
AvailableSpace::MaxContent => 300.0,
},
height: resolved_line_height.unwrap_or(16.0),
},
);
}
_ => {}
}
}
if is_replaced_element(&element_data.name.local) {
let mut attr_size = taffy::Size {
width: element_data
.attr(local_name!("width"))
.and_then(|val| val.parse::<f32>().ok()),
height: element_data
.attr(local_name!("height"))
.and_then(|val| val.parse::<f32>().ok()),
};
let (inherent_size, inherent_ratio) = match &element_data.special_data {
SpecialElementData::Image(image_data) => match &**image_data {
ImageData::Raster(image) => {
let size = taffy::Size {
width: image.width as f32,
height: image.height as f32,
};
(size, Some(size.width / size.height))
}
#[cfg(feature = "svg")]
ImageData::Svg(svg) => {
if *element_data.name.local == local_name!("svg") {
attr_size = taffy::Size {
width: svg.resolved_width(inputs.parent_size.width),
height: svg.resolved_height(inputs.parent_size.height),
};
}
let (mut width, mut height) = svg.intrinsic_size();
if svg.intrinsic_width().is_none()
&& svg.intrinsic_height().is_none()
{
if let (
Some(ratio),
AvailableSpace::Definite(available_width),
) =
(svg.viewbox_aspect_ratio(), inputs.available_space.width)
{
width = available_width;
height = available_width / ratio;
}
}
(taffy::Size { width, height }, Some(svg.aspect_ratio()))
}
ImageData::None => (taffy::Size::ZERO, None),
},
SpecialElementData::Canvas(_)
| SpecialElementData::SubDocument(_)
| SpecialElementData::None => {
let tag_name = &element_data.name.local;
if *tag_name == local_name!("img") || *tag_name == local_name!("svg") {
(taffy::Size::ZERO, None)
} else {
let size = taffy::Size {
width: attr_size.width.unwrap_or(300.0),
height: attr_size.height.unwrap_or(150.0),
};
let ratio = (*tag_name == local_name!("canvas"))
.then(|| size.width / size.height);
(size, ratio)
}
}
_ => unreachable!(),
};
let replaced_context = ReplacedContext {
inherent_size,
attr_size,
inherent_ratio,
};
let computed = replaced_measure_function(
inputs.known_dimensions,
inputs.parent_size,
inputs.available_space,
&replaced_context,
node.style(),
inputs.sizing_mode,
inputs.axis,
);
return taffy::LayoutOutput {
size: computed,
content_size: computed,
first_baselines: taffy::Point::NONE,
top_margin: CollapsibleMarginSet::ZERO,
bottom_margin: CollapsibleMarginSet::ZERO,
margins_can_collapse_through: false,
};
}
if node.flags.is_table_root() {
let SpecialElementData::TableRoot(context) = &self.nodes[dom_node_id(node_id)]
.data
.downcast_element()
.unwrap()
.special_data
else {
panic!("Node marked as table root but doesn't have TableContext");
};
let context = Arc::clone(context);
let mut table_wrapper = TableTreeWrapper {
doc: self,
ctx: context,
};
let mut output = compute_grid_layout(&mut table_wrapper, node_id, inputs);
output.content_size.width = output.content_size.width.min(output.size.width);
output.content_size.height = output.content_size.height.min(output.size.height);
return output;
}
if node.flags.is_inline_root() {
return self.compute_inline_layout(dom_node_id(node_id), inputs, block_ctx);
}
match node.style().display {
Display::Block => compute_block_layout(self, node_id, inputs, block_ctx),
Display::FlowRoot => compute_block_layout(self, node_id, inputs, None),
Display::Flex => compute_flexbox_layout(self, node_id, inputs),
Display::Grid => compute_grid_layout(self, node_id, inputs),
Display::None => taffy::LayoutOutput::HIDDEN,
}
}
NodeData::Document(_) => compute_block_layout(self, node_id, inputs, None),
_ => taffy::LayoutOutput::HIDDEN,
}
}
}
impl TraversePartialTree for BaseDocument {
type ChildIter<'a> = RefCellChildIter<'a>;
fn child_ids(&self, node_id: NodeId) -> Self::ChildIter<'_> {
let layout_children = self.node_from_id(node_id).layout_children.borrow(); RefCellChildIter::new(Ref::map(layout_children, |children| {
children.as_ref().map(|c| c.as_slice()).unwrap_or(&[])
}))
}
fn child_count(&self, node_id: NodeId) -> usize {
self.node_from_id(node_id)
.layout_children
.borrow()
.as_ref()
.map(|c| c.len())
.unwrap_or(0)
}
fn get_child_id(&self, node_id: NodeId, index: usize) -> NodeId {
taffy_node_id(
self.node_from_id(node_id)
.layout_children
.borrow()
.as_ref()
.unwrap()[index],
)
}
}
impl TraverseTree for BaseDocument {}
impl LayoutPartialTree for BaseDocument {
type CoreContainerStyle<'a>
= &'a taffy::Style<Atom>
where
Self: 'a;
type CustomIdent = Atom;
fn get_core_container_style(&self, node_id: NodeId) -> &Style<Atom> {
self.node_from_id(node_id).style()
}
fn set_unrounded_layout(&mut self, node_id: NodeId, layout: &Layout) {
*self.node_from_id_mut(node_id).unrounded_layout_mut() = *layout;
}
fn resolve_calc_value(&self, calc_ptr: *const (), parent_size: f32) -> f32 {
resolve_calc_value(calc_ptr, parent_size)
}
#[inline(always)]
fn compute_child_layout(
&mut self,
node_id: NodeId,
inputs: taffy::LayoutInput,
) -> taffy::LayoutOutput {
#[cfg(not(target_arch = "wasm32"))]
let probing = layout_panic_probe::enabled();
#[cfg(not(target_arch = "wasm32"))]
if probing {
layout_panic_probe::push(
dom_node_id(node_id),
self.describe_node_for_panic(dom_node_id(node_id), &inputs),
);
}
let output = compute_cached_layout(self, node_id, inputs, |tree, node_id, inputs| {
tree.compute_child_layout_internal(node_id, inputs, None)
});
#[cfg(not(target_arch = "wasm32"))]
if probing {
layout_panic_probe::pop();
}
output
}
}
impl taffy::CacheTree for BaseDocument {
#[inline]
fn cache_get(
&self,
node_id: NodeId,
inputs: &taffy::LayoutInput,
) -> Option<taffy::LayoutOutput> {
let found = self.node_from_id(node_id).cache().get(inputs);
#[cfg(feature = "log-phase-times")]
layout_counters::note_lookup(found.is_some());
found
}
#[inline]
fn cache_store(
&mut self,
node_id: NodeId,
inputs: &taffy::LayoutInput,
layout_output: taffy::LayoutOutput,
) {
self.node_from_id_mut(node_id)
.cache_mut()
.store(inputs, layout_output);
}
#[inline]
fn cache_clear(&mut self, node_id: NodeId) {
self.node_from_id_mut(node_id).cache_release();
}
}
impl taffy::LayoutBlockContainer for BaseDocument {
type BlockContainerStyle<'a>
= &'a Style<Atom>
where
Self: 'a;
type BlockItemStyle<'a>
= &'a Style<Atom>
where
Self: 'a;
fn get_block_container_style(&self, node_id: NodeId) -> Self::BlockContainerStyle<'_> {
self.get_core_container_style(node_id)
}
fn get_block_child_style(&self, child_node_id: NodeId) -> Self::BlockItemStyle<'_> {
self.get_core_container_style(child_node_id)
}
#[inline(always)]
fn compute_block_child_layout(
&mut self,
node_id: NodeId,
inputs: taffy::LayoutInput,
block_ctx: Option<&mut BlockContext<'_>>,
) -> taffy::LayoutOutput {
compute_cached_layout(self, node_id, inputs, |tree, node_id, inputs| {
tree.compute_child_layout_internal(node_id, inputs, block_ctx)
})
}
}
impl taffy::LayoutFlexboxContainer for BaseDocument {
type FlexboxContainerStyle<'a>
= &'a Style<Atom>
where
Self: 'a;
type FlexboxItemStyle<'a>
= &'a Style<Atom>
where
Self: 'a;
fn get_flexbox_container_style(&self, node_id: NodeId) -> Self::FlexboxContainerStyle<'_> {
self.get_core_container_style(node_id)
}
fn get_flexbox_child_style(&self, child_node_id: NodeId) -> Self::FlexboxItemStyle<'_> {
self.get_core_container_style(child_node_id)
}
}
impl taffy::LayoutGridContainer for BaseDocument {
type GridContainerStyle<'a>
= &'a Style<Atom>
where
Self: 'a;
type GridItemStyle<'a>
= &'a Style<Atom>
where
Self: 'a;
fn get_grid_container_style(&self, node_id: NodeId) -> Self::GridContainerStyle<'_> {
self.get_core_container_style(node_id)
}
fn get_grid_child_style(&self, child_node_id: NodeId) -> Self::GridItemStyle<'_> {
self.get_core_container_style(child_node_id)
}
fn set_detailed_grid_info(
&mut self,
node_id: NodeId,
detailed_grid_info: taffy::DetailedGridInfo,
) {
let node = self.node_from_id_mut(node_id);
if let Some(element) = node.element_data_mut() {
element.detailed_grid_info = Some(Box::new(detailed_grid_info));
}
}
}
impl RoundTree for BaseDocument {
fn get_unrounded_layout(&self, node_id: NodeId) -> Layout {
*self.node_from_id(node_id).unrounded_layout()
}
fn set_final_layout(&mut self, node_id: NodeId, layout: &Layout) {
*self.node_from_id_mut(node_id).final_layout_mut() = *layout;
}
}
impl PrintTree for BaseDocument {
fn get_debug_label(&self, node_id: NodeId) -> &'static str {
let node = &self.node_from_id(node_id);
match node.data {
NodeData::Document(_) => "DOCUMENT",
NodeData::Text { .. } => node.node_debug_str().leak(),
NodeData::Comment { .. } => "COMMENT",
NodeData::DocumentFragment => "FRAGMENT",
NodeData::ShadowRoot(_) => "SHADOW ROOT",
NodeData::AnonymousBlock(_) => "ANONYMOUS BLOCK",
NodeData::Element(_) => {
let style = node.style();
let display = match style.display {
Display::Flex => match style.flex_direction {
FlexDirection::Row | FlexDirection::RowReverse => "FLEX ROW",
FlexDirection::Column | FlexDirection::ColumnReverse => "FLEX COL",
},
Display::Grid => "GRID",
Display::Block => "BLOCK",
Display::FlowRoot => "FLOW ROOT",
Display::None => "NONE",
};
format!("{} ({})", node.node_debug_str(), display).leak()
} }
}
fn get_final_layout(&self, node_id: NodeId) -> Layout {
*self.node_from_id(node_id).final_layout()
}
}
pub struct RefCellChildIter<'a> {
items: Ref<'a, [crate::NodeId]>,
idx: usize,
}
impl<'a> RefCellChildIter<'a> {
fn new(items: Ref<'a, [crate::NodeId]>) -> RefCellChildIter<'a> {
RefCellChildIter { items, idx: 0 }
}
}
impl Iterator for RefCellChildIter<'_> {
type Item = NodeId;
fn next(&mut self) -> Option<Self::Item> {
self.items.get(self.idx).map(|id| {
self.idx += 1;
taffy_node_id(*id)
})
}
}