use std::{
collections::{BTreeMap, BTreeSet, HashMap},
hash::{DefaultHasher, Hash, Hasher},
};
const CACHE_SIZE_EPSILON: f32 = 0.1;
use azul_core::{
diff::NodeDataFingerprint,
dom::{FormattingContext, NodeId, NodeType},
geom::{LogicalPosition, LogicalRect, LogicalSize},
styled_dom::{StyledDom, StyledNode},
};
use azul_css::{
css::CssPropertyValue,
props::{
layout::{
LayoutDisplay, LayoutHeight, LayoutOverflow,
LayoutPosition, LayoutWritingMode,
},
property::{CssProperty, CssPropertyType},
style::StyleTextAlign,
},
LayoutDebugMessage, LayoutDebugMessageType,
};
use crate::{
font_traits::{FontLoaderTrait, ParsedFontTrait, TextLayoutCache},
solver3::{
fc::{self, layout_formatting_context, LayoutConstraints, OverflowBehavior},
geometry::PositionedRectangle,
getters::{
get_css_height, get_display_property, get_overflow_x,
get_overflow_y, get_scrollbar_gutter_property, get_text_align, get_white_space_property, get_writing_mode,
MultiValue,
},
layout_tree::{
get_display_type, is_block_level, AnonymousBoxType, DirtyFlag, LayoutNode, LayoutNodeHot, LayoutTreeBuilder, SubtreeHash,
},
positioning::get_position_type,
scrollbar::ScrollbarRequirements,
sizing::calculate_used_size_for_node,
LayoutContext, LayoutError, LayoutTree, Result,
},
text3::cache::AvailableSpace as Text3AvailableSpace,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComputeMode {
ComputeSize,
PerformLayout,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AvailableWidthType {
Definite,
MinContent,
MaxContent,
}
#[derive(Copy, Debug, Clone)]
pub struct SizingCacheEntry {
pub available_size: LogicalSize,
pub result_size: LogicalSize,
pub baseline: Option<f32>,
pub escaped_top_margin: Option<f32>,
pub escaped_bottom_margin: Option<f32>,
}
#[derive(Debug, Clone)]
pub struct LayoutCacheEntry {
pub available_size: LogicalSize,
pub result_size: LogicalSize,
pub content_size: LogicalSize,
pub child_positions: Vec<(usize, LogicalPosition)>,
pub escaped_top_margin: Option<f32>,
pub escaped_bottom_margin: Option<f32>,
pub scrollbar_info: ScrollbarRequirements,
}
#[derive(Debug, Clone)]
pub struct NodeCache {
pub measure_entries: [Option<SizingCacheEntry>; 9],
pub layout_entry: Option<LayoutCacheEntry>,
pub is_empty: bool,
}
impl Default for NodeCache {
fn default() -> Self {
Self {
measure_entries: [None, None, None, None, None, None, None, None, None],
layout_entry: None,
is_empty: true, }
}
}
impl NodeCache {
pub fn clear(&mut self) {
self.measure_entries = [None, None, None, None, None, None, None, None, None];
self.layout_entry = None;
self.is_empty = true;
}
#[must_use] pub fn slot_index(
width_known: bool,
height_known: bool,
width_type: AvailableWidthType,
height_type: AvailableWidthType,
) -> usize {
match (width_known, height_known) {
(true, true) => 0,
(true, false) => {
if width_type == AvailableWidthType::MinContent { 2 } else { 1 }
}
(false, true) => {
if height_type == AvailableWidthType::MinContent { 4 } else { 3 }
}
(false, false) => {
let w = usize::from(width_type == AvailableWidthType::MinContent);
let h = usize::from(height_type == AvailableWidthType::MinContent);
5 + w * 2 + h
}
}
}
#[must_use] pub fn get_size(&self, slot: usize, known_dims: LogicalSize) -> Option<&SizingCacheEntry> {
let entry = self.measure_entries[slot].as_ref()?;
if (known_dims.width - entry.available_size.width).abs() < CACHE_SIZE_EPSILON
&& (known_dims.height - entry.available_size.height).abs() < CACHE_SIZE_EPSILON
{
return Some(entry);
}
if (known_dims.width - entry.result_size.width).abs() < CACHE_SIZE_EPSILON
&& (known_dims.height - entry.result_size.height).abs() < CACHE_SIZE_EPSILON
{
return Some(entry);
}
None
}
pub const fn store_size(&mut self, slot: usize, entry: SizingCacheEntry) {
self.measure_entries[slot] = Some(entry);
self.is_empty = false;
}
#[must_use] pub fn get_layout(&self, known_dims: LogicalSize) -> Option<&LayoutCacheEntry> {
let entry = self.layout_entry.as_ref()?;
if (known_dims.width - entry.available_size.width).abs() < CACHE_SIZE_EPSILON
&& (known_dims.height - entry.available_size.height).abs() < CACHE_SIZE_EPSILON
{
return Some(entry);
}
if (known_dims.width - entry.result_size.width).abs() < CACHE_SIZE_EPSILON
&& (known_dims.height - entry.result_size.height).abs() < CACHE_SIZE_EPSILON
{
return Some(entry);
}
None
}
pub fn store_layout(&mut self, entry: LayoutCacheEntry) {
self.layout_entry = Some(entry);
self.is_empty = false;
}
}
#[derive(Debug, Clone, Default)]
pub struct LayoutCacheMap {
pub entries: Vec<NodeCache>,
}
impl LayoutCacheMap {
pub fn resize_to_tree(&mut self, tree_len: usize) {
self.entries.resize_with(tree_len, NodeCache::default);
}
#[inline]
#[must_use] pub fn get(&self, node_index: usize) -> &NodeCache {
&self.entries[node_index]
}
#[inline]
pub fn get_mut(&mut self, node_index: usize) -> &mut NodeCache {
&mut self.entries[node_index]
}
pub fn mark_dirty(&mut self, node_index: usize, tree: &[LayoutNodeHot]) {
if node_index >= self.entries.len() {
return;
}
let cache = &mut self.entries[node_index];
if cache.is_empty {
return; }
cache.clear();
let mut current = tree.get(node_index).and_then(|n| n.parent);
while let Some(parent_idx) = current {
if parent_idx >= self.entries.len() {
break;
}
let parent_cache = &mut self.entries[parent_idx];
if parent_cache.is_empty {
break; }
parent_cache.clear();
current = tree.get(parent_idx).and_then(|n| n.parent);
}
}
}
#[derive(Debug, Clone, Default)]
pub struct LayoutCache {
pub tree: Option<LayoutTree>,
pub calculated_positions: super::PositionVec,
pub viewport: Option<LogicalRect>,
pub scroll_ids: HashMap<usize, u64>,
pub scroll_id_to_node_id: HashMap<u64, NodeId>,
pub counters: HashMap<(usize, String), i32>,
pub float_cache: HashMap<usize, fc::FloatingContext>,
pub cache_map: LayoutCacheMap,
pub previous_positions: super::PositionVec,
pub cached_display_list: Option<(SubtreeHash, LogicalRect, super::display_list::DisplayList)>,
pub prev_dom_ptr: usize,
pub prev_viewport: LogicalRect,
}
#[derive(Copy, Debug, Clone, Default)]
pub struct Solver3CacheMemoryReport {
pub tree_bytes: usize,
pub tree_report: Option<super::layout_tree::LayoutTreeMemoryReport>,
pub calculated_positions_bytes: usize,
pub previous_positions_bytes: usize,
pub scroll_ids_bytes: usize,
pub scroll_id_to_node_id_bytes: usize,
pub counters_bytes: usize,
pub float_cache_bytes: usize,
pub cache_map_bytes: usize,
pub cached_display_list_bytes: usize,
}
impl Solver3CacheMemoryReport {
#[must_use] pub const fn total_bytes(&self) -> usize {
self.tree_bytes
+ self.calculated_positions_bytes
+ self.previous_positions_bytes
+ self.scroll_ids_bytes
+ self.scroll_id_to_node_id_bytes
+ self.counters_bytes
+ self.float_cache_bytes
+ self.cache_map_bytes
+ self.cached_display_list_bytes
}
}
impl LayoutCache {
pub fn reset_incremental(&mut self) {
self.tree = None;
self.cache_map = LayoutCacheMap::default();
self.cached_display_list = None;
self.prev_dom_ptr = 0;
self.counters.clear();
self.float_cache.clear();
}
#[must_use] pub fn memory_report(&self) -> Solver3CacheMemoryReport {
let tree_report = self.tree.as_ref().map(LayoutTree::memory_report);
let tree_bytes = tree_report.as_ref().map_or(0, super::layout_tree::LayoutTreeMemoryReport::total_bytes);
let mut cache_map_bytes = self.cache_map.entries.capacity()
* size_of::<NodeCache>();
for e in &self.cache_map.entries {
if let Some(le) = &e.layout_entry {
cache_map_bytes += le.child_positions.capacity()
* size_of::<(usize, LogicalPosition)>();
}
}
Solver3CacheMemoryReport {
tree_bytes,
tree_report,
calculated_positions_bytes: self.calculated_positions.len()
* size_of::<LogicalPosition>(),
previous_positions_bytes: self.previous_positions.len()
* size_of::<LogicalPosition>(),
scroll_ids_bytes: self.scroll_ids.len()
* (size_of::<usize>() + size_of::<u64>()),
scroll_id_to_node_id_bytes: self.scroll_id_to_node_id.len()
* (size_of::<u64>() + size_of::<NodeId>()),
counters_bytes: self.counters.iter().map(|((_, name), _)| {
size_of::<(usize, String)>()
+ size_of::<i32>()
+ name.capacity()
}).sum(),
float_cache_bytes: self.float_cache.len() * 256, cache_map_bytes,
cached_display_list_bytes: if self.cached_display_list.is_some() { 2048 } else { 0 },
}
}
}
#[derive(Debug, Default)]
pub struct ReconciliationResult {
pub intrinsic_dirty: BTreeSet<usize>,
pub layout_roots: BTreeSet<usize>,
pub paint_dirty: BTreeSet<usize>,
}
impl ReconciliationResult {
#[must_use] pub fn is_clean(&self) -> bool {
self.intrinsic_dirty.is_empty()
&& self.layout_roots.is_empty()
&& self.paint_dirty.is_empty()
}
#[must_use] pub fn needs_layout(&self) -> bool {
!self.intrinsic_dirty.is_empty() || !self.layout_roots.is_empty()
}
#[must_use] pub fn needs_paint_only(&self) -> bool {
!self.needs_layout() && !self.paint_dirty.is_empty()
}
}
#[allow(clippy::match_same_arms)] pub fn reposition_clean_subtrees(
styled_dom: &StyledDom,
tree: &LayoutTree,
layout_roots: &BTreeSet<usize>,
calculated_positions: &mut super::PositionVec,
) {
let mut parents_to_reposition = BTreeSet::new();
for &root_idx in layout_roots {
if let Some(parent_idx) = tree.get(root_idx).and_then(|n| n.parent) {
parents_to_reposition.insert(parent_idx);
}
}
for parent_idx in parents_to_reposition {
let Some(parent_node) = tree.get(parent_idx) else {
continue;
};
match parent_node.formatting_context {
FormattingContext::Block { .. } | FormattingContext::TableRowGroup => {
reposition_block_flow_siblings(
styled_dom,
parent_idx,
tree,
layout_roots,
calculated_positions,
);
}
FormattingContext::Flex | FormattingContext::Grid => {
}
FormattingContext::Table | FormattingContext::TableRow => {
}
_ => { }
}
}
}
fn to_overflow_behavior(overflow: MultiValue<LayoutOverflow>) -> fc::OverflowBehavior {
match overflow.unwrap_or(LayoutOverflow::Visible) {
LayoutOverflow::Visible => fc::OverflowBehavior::Visible,
LayoutOverflow::Hidden | LayoutOverflow::Clip => fc::OverflowBehavior::Hidden,
LayoutOverflow::Scroll => fc::OverflowBehavior::Scroll,
LayoutOverflow::Auto => fc::OverflowBehavior::Auto,
}
}
const fn style_text_align_to_fc(text_align: StyleTextAlign) -> fc::TextAlign {
match text_align {
StyleTextAlign::Start | StyleTextAlign::Left => fc::TextAlign::Start,
StyleTextAlign::End | StyleTextAlign::Right => fc::TextAlign::End,
StyleTextAlign::Center => fc::TextAlign::Center,
StyleTextAlign::Justify => fc::TextAlign::Justify,
}
}
#[allow(clippy::cast_possible_truncation)] #[must_use] pub fn collect_children_dom_ids(styled_dom: &StyledDom, parent_dom_id: NodeId) -> Vec<NodeId> {
let hierarchy_container = styled_dom.node_hierarchy.as_container();
let mut children = Vec::new();
let Some(hierarchy_item) = hierarchy_container.get(parent_dom_id) else {
return children;
};
let Some(mut child_id) = hierarchy_item.first_child_id(parent_dom_id) else {
unsafe {
let pi = parent_dom_id.index();
if pi < 8 { crate::az_mark((0x40540 + pi * 4) as u32, (0xC000_0000u32)); }
}
return children;
};
if get_display_type(styled_dom, child_id) != LayoutDisplay::None {
children.push(child_id);
}
while let Some(hierarchy_item) = hierarchy_container.get(child_id) {
let Some(next) = hierarchy_item.next_sibling_id() else {
break;
};
if get_display_type(styled_dom, next) != LayoutDisplay::None {
children.push(next);
}
child_id = next;
}
unsafe {
let pi = parent_dom_id.index();
if pi < 8 {
crate::az_mark((0x40540 + pi * 4) as u32, (0xCC00_0000u32 | (children.len() as u32 & 0xffff)));
}
}
children
}
pub fn reposition_block_flow_siblings(
styled_dom: &StyledDom,
parent_idx: usize,
tree: &LayoutTree,
layout_roots: &BTreeSet<usize>,
calculated_positions: &mut super::PositionVec,
) {
let Some(parent_node) = tree.get(parent_idx) else {
return;
};
let dom_id = parent_node.dom_node_id.unwrap_or(NodeId::ZERO);
let styled_node_state = styled_dom
.styled_nodes
.as_container()
.get(dom_id)
.map(|n| n.styled_node_state)
.unwrap_or_default();
let writing_mode = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
let parent_pos = calculated_positions
.get(parent_idx)
.copied()
.unwrap_or_default();
let parent_bp = parent_node.box_props.unpack();
let content_box_origin = LogicalPosition::new(
parent_pos.x + parent_bp.padding.left,
parent_pos.y + parent_bp.padding.top,
);
let mut main_pen = 0.0;
for &child_idx in tree.children(parent_idx) {
let Some(child_node) = tree.get(child_idx) else {
continue;
};
let child_size = child_node.used_size.unwrap_or_default();
let child_bp = child_node.box_props.unpack();
let child_main_sum = child_bp.margin.main_sum(writing_mode);
let margin_box_main_size = child_size.main(writing_mode) + child_main_sum;
if layout_roots.contains(&child_idx) {
let new_pos = match calculated_positions.get(child_idx) {
Some(p) => *p,
None => continue,
};
let main_axis_offset = if writing_mode.is_vertical() {
new_pos.x - content_box_origin.x
} else {
new_pos.y - content_box_origin.y
};
main_pen = main_axis_offset
+ child_size.main(writing_mode)
+ child_bp.margin.main_end(writing_mode);
} else {
let old_pos = match calculated_positions.get(child_idx) {
Some(p) => *p,
None => continue,
};
let child_main_start = child_bp.margin.main_start(writing_mode);
let new_main_pos = main_pen + child_main_start;
let old_relative_pos = tree.warm(child_idx)
.and_then(|w| w.relative_position)
.unwrap_or_default();
let cross_pos = if writing_mode.is_vertical() {
old_relative_pos.y
} else {
old_relative_pos.x
};
let new_relative_pos =
LogicalPosition::from_main_cross(new_main_pos, cross_pos, writing_mode);
let new_absolute_pos = LogicalPosition::new(
content_box_origin.x + new_relative_pos.x,
content_box_origin.y + new_relative_pos.y,
);
if old_pos != new_absolute_pos {
let delta = LogicalPosition::new(
new_absolute_pos.x - old_pos.x,
new_absolute_pos.y - old_pos.y,
);
shift_subtree_position(child_idx, delta, tree, calculated_positions);
}
main_pen += margin_box_main_size;
}
}
}
fn shift_subtree_position(
node_idx: usize,
delta: LogicalPosition,
tree: &LayoutTree,
calculated_positions: &mut super::PositionVec,
) {
if let Some(pos) = calculated_positions.get_mut(node_idx) {
pos.x += delta.x;
pos.y += delta.y;
}
if let Some(node) = tree.get(node_idx) {
let children = tree.children(node_idx).to_vec();
for &child_idx in &children {
shift_subtree_position(child_idx, delta, tree, calculated_positions);
}
}
}
fn layout_relevant_child_count(
styled_dom: &StyledDom,
children: &[NodeId],
parent_id: NodeId,
) -> usize {
use super::getters::{get_display_property, MultiValue};
use super::layout_tree::{is_block_level, is_whitespace_only_text};
let parent_display = match get_display_property(styled_dom, Some(parent_id)) {
MultiValue::Exact(d) => d,
_ => LayoutDisplay::Block,
};
let is_table_structural = matches!(
parent_display,
LayoutDisplay::Table
| LayoutDisplay::InlineTable
| LayoutDisplay::TableRowGroup
| LayoutDisplay::TableHeaderGroup
| LayoutDisplay::TableFooterGroup
| LayoutDisplay::TableRow
);
let has_any_block_child = children
.iter()
.any(|&id| is_block_level(styled_dom, id));
let mut count = 0usize;
let collapse_inline_whitespace = has_any_block_child;
for &id in children {
let display = match get_display_property(styled_dom, Some(id)) {
MultiValue::Exact(d) => d,
_ => LayoutDisplay::Block,
};
if matches!(display, LayoutDisplay::None) {
continue;
}
if is_table_structural && is_whitespace_only_text(styled_dom, id) {
continue;
}
if collapse_inline_whitespace
&& !is_block_level(styled_dom, id)
&& is_whitespace_only_text(styled_dom, id)
{
continue;
}
count += 1;
}
count
}
pub fn reconcile_and_invalidate<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
cache: &LayoutCache,
viewport: LogicalRect,
) -> Result<(LayoutTree, ReconciliationResult)> {
let _probe_outer = crate::probe::Probe::span("reconcile_and_invalidate");
let mut new_tree_builder = LayoutTreeBuilder::new(ctx.viewport_size);
let mut recon_result = ReconciliationResult::default();
let viewport_resized = cache.viewport.is_none_or(|v| v.size != viewport.size);
let old_tree = if viewport_resized {
None
} else {
cache.tree.as_ref()
};
if viewport_resized {
recon_result.layout_roots.insert(0); }
let root_dom_id = ctx
.styled_dom
.root
.into_crate_internal()
.unwrap_or(NodeId::ZERO);
let root_idx = reconcile_recursive(
ctx.styled_dom,
root_dom_id,
old_tree.map(|t| t.root),
None,
old_tree,
&mut new_tree_builder,
&mut recon_result,
ctx.debug_messages,
)?;
let final_layout_roots = recon_result
.layout_roots
.iter()
.filter(|&&idx| {
let mut current = new_tree_builder.get(idx).and_then(|n| n.parent);
while let Some(p_idx) = current {
if recon_result.layout_roots.contains(&p_idx) {
return false;
}
current = new_tree_builder.get(p_idx).and_then(|n| n.parent);
}
true
})
.copied()
.collect();
recon_result.layout_roots = final_layout_roots;
let new_tree = new_tree_builder.build(root_idx);
{ let _ = (0xCC00_0001u32); }
Ok((new_tree, recon_result))
}
fn is_whitespace_only_inline_run(
styled_dom: &StyledDom,
inline_run: &[(usize, NodeId)],
parent_dom_id: NodeId,
) -> bool {
use azul_css::props::style::text::StyleWhiteSpace;
if inline_run.is_empty() {
return true;
}
let parent_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
let white_space = match get_white_space_property(styled_dom, parent_dom_id, parent_state) {
MultiValue::Exact(ws) => Some(ws),
_ => None,
};
if matches!(
white_space,
Some(StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap |
StyleWhiteSpace::PreLine)
) {
return false;
}
let binding = styled_dom.node_data.as_container();
for &(_, dom_id) in inline_run {
if let Some(data) = binding.get(dom_id) {
match data.get_node_type() {
NodeType::Text(text) => {
let s = text.as_str();
if !s.chars().all(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')) {
return false; }
}
_ => {
return false; }
}
}
}
true }
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn reconcile_recursive(
styled_dom: &StyledDom,
new_dom_id: NodeId,
old_tree_idx: Option<usize>,
new_parent_idx: Option<usize>,
old_tree: Option<&LayoutTree>,
new_tree_builder: &mut LayoutTreeBuilder,
recon: &mut ReconciliationResult,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> Result<usize> {
static FP_DUMP_ENABLED: std::sync::OnceLock<bool> =
std::sync::OnceLock::new();
let node_data = &styled_dom.node_data.as_container()[new_dom_id];
let old_cold = old_tree.and_then(|t| old_tree_idx.and_then(|idx| t.cold(idx)));
match (old_tree.is_some(), old_tree_idx.is_some(), old_cold.is_some()) {
(false, _, _) => drop(crate::probe::Probe::span("recon_old_tree_none")),
(true, false, _) => drop(crate::probe::Probe::span("recon_old_idx_none")),
(true, true, false) => drop(crate::probe::Probe::span("recon_cold_none")),
(true, true, true) => drop(crate::probe::Probe::span("recon_cold_some")),
}
let new_fingerprint = {
let _p = crate::probe::Probe::span("fingerprint_compute");
NodeDataFingerprint::compute(
node_data,
styled_dom.styled_nodes.as_container().get(new_dom_id).map(|n| &n.styled_node_state),
)
};
let dirty_flag = old_cold.map_or_else(|| {
drop(crate::probe::Probe::span("fp_new_node"));
DirtyFlag::Layout }, |old_c| {
let change_set = old_c.node_data_fingerprint.diff(&new_fingerprint);
if change_set.needs_layout() {
drop(crate::probe::Probe::span("fp_needs_layout"));
let enabled = *FP_DUMP_ENABLED.get_or_init(|| {
std::env::var_os("AZ_FP_DUMP").is_some()
});
if enabled {
use std::sync::atomic::{AtomicUsize, Ordering};
static DUMPED: AtomicUsize = AtomicUsize::new(0);
let n = DUMPED.fetch_add(1, Ordering::Relaxed);
if n < 10 {
eprintln!(
"[fp_diff {n}] dom={} old={:?} new={:?}",
new_dom_id.index(),
old_c.node_data_fingerprint,
new_fingerprint,
);
}
}
DirtyFlag::Layout
} else if change_set.needs_paint() {
drop(crate::probe::Probe::span("fp_needs_paint"));
DirtyFlag::Paint
} else {
drop(crate::probe::Probe::span("fp_clean"));
DirtyFlag::None
}
});
let is_dirty = dirty_flag >= DirtyFlag::Paint;
let new_node_idx = if dirty_flag >= DirtyFlag::Layout || old_tree.is_none() {
{ let _ = (0xBB00_0001u32); }
let idx = new_tree_builder.create_node_from_dom(
styled_dom,
new_dom_id,
new_parent_idx,
debug_messages,
);
new_tree_builder.blockify_node_display(styled_dom, new_dom_id, idx, new_parent_idx);
idx
} else {
{ let _ = (0xBB00_0002u32); }
let old_full_node = old_tree
.and_then(|t| old_tree_idx.and_then(|idx| t.get_full_node(idx)))
.ok_or(LayoutError::InvalidTree)?;
let mut idx = new_tree_builder.clone_node_from_old(&old_full_node, new_parent_idx);
if dirty_flag == DirtyFlag::Paint {
if let Some(cloned) = new_tree_builder.get_mut(idx) {
cloned.node_data_fingerprint = new_fingerprint;
cloned.dirty_flag = DirtyFlag::Paint;
}
}
idx
};
{ let _ = (0xAB00_0000u32 | (new_node_idx as u32 & 0xffff)); }
{
use crate::solver3::getters::get_display_property;
let display = get_display_property(styled_dom, Some(new_dom_id))
.exact();
if matches!(display, Some(LayoutDisplay::ListItem)) {
new_tree_builder.create_marker_pseudo_element(styled_dom, new_dom_id, new_node_idx);
}
}
let mut new_children_dom_ids: Vec<_> = collect_children_dom_ids(styled_dom, new_dom_id);
{
use super::getters::{get_display_property, MultiValue};
let parent_display = match get_display_property(styled_dom, Some(new_dom_id)) {
MultiValue::Exact(d) => d,
_ => LayoutDisplay::Block,
};
if matches!(parent_display,
LayoutDisplay::Table
| LayoutDisplay::InlineTable
| LayoutDisplay::TableRowGroup
| LayoutDisplay::TableHeaderGroup
| LayoutDisplay::TableFooterGroup
| LayoutDisplay::TableRow
) {
new_children_dom_ids.retain(|&id| {
!super::layout_tree::is_whitespace_only_text(styled_dom, id)
});
}
}
let old_children_indices: Vec<usize> = old_tree
.and_then(|t| old_tree_idx.map(|idx| t.children(idx).to_vec()))
.unwrap_or_default();
let old_children_by_dom: alloc::collections::BTreeMap<NodeId, usize> = old_tree
.and_then(|t| old_tree_idx.map(|idx| {
t.children(idx).iter()
.filter_map(|&cidx| t.get(cidx).and_then(|n| n.dom_node_id).map(|did| (did, cidx)))
.collect()
}))
.unwrap_or_default();
let old_layout_relevant_count = old_children_by_dom.len();
let new_layout_relevant_count = layout_relevant_child_count(styled_dom, &new_children_dom_ids, new_dom_id);
let mut children_are_different = new_layout_relevant_count != old_layout_relevant_count;
let mut new_child_hashes = Vec::new();
let has_block_child = new_children_dom_ids
.iter()
.any(|&id| is_block_level(styled_dom, id));
let parent_is_flex_or_grid = matches!(
get_display_type(styled_dom, new_dom_id),
LayoutDisplay::Flex
| LayoutDisplay::InlineFlex
| LayoutDisplay::Grid
| LayoutDisplay::InlineGrid
);
if !has_block_child || parent_is_flex_or_grid {
for (i, &new_child_dom_id) in new_children_dom_ids.iter().enumerate() {
let old_child_idx = old_children_by_dom.get(&new_child_dom_id).copied();
let reconciled_child_idx = reconcile_recursive(
styled_dom,
new_child_dom_id,
old_child_idx,
Some(new_node_idx),
old_tree,
new_tree_builder,
recon,
debug_messages,
)?;
if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
new_child_hashes.push(child_node.subtree_hash.0);
}
if old_tree.and_then(|t| t.cold(old_child_idx?).map(|n| n.subtree_hash))
!= new_tree_builder
.get(reconciled_child_idx)
.map(|n| n.subtree_hash)
{
children_are_different = true;
}
}
} else {
if let Some(msgs) = debug_messages.as_mut() {
msgs.push(LayoutDebugMessage::info(format!(
"[reconcile_recursive] Mixed content in node {}: creating anonymous IFC wrappers",
new_dom_id.index()
)));
}
let mut inline_run: Vec<(usize, NodeId)> = Vec::new();
for (i, &new_child_dom_id) in new_children_dom_ids.iter().enumerate() {
if is_block_level(styled_dom, new_child_dom_id) {
if !inline_run.is_empty() {
if is_whitespace_only_inline_run(styled_dom, &inline_run, new_dom_id) {
if let Some(msgs) = debug_messages.as_mut() {
msgs.push(LayoutDebugMessage::info(format!(
"[reconcile_recursive] Skipping whitespace-only inline run ({} nodes) between blocks in node {}",
inline_run.len(),
new_dom_id.index()
)));
}
inline_run.clear();
} else {
let anon_idx = new_tree_builder.create_anonymous_node(
new_node_idx,
AnonymousBoxType::InlineWrapper,
FormattingContext::Inline, );
if let Some(msgs) = debug_messages.as_mut() {
msgs.push(LayoutDebugMessage::info(format!(
"[reconcile_recursive] Created anonymous IFC wrapper (layout_idx={}) for {} inline children: {:?}",
anon_idx,
inline_run.len(),
inline_run.iter().map(|(_, id)| id.index()).collect::<Vec<_>>()
)));
}
#[allow(clippy::iter_with_drain)] for (pos, inline_dom_id) in inline_run.drain(..) {
let old_child_idx = old_children_by_dom.get(&inline_dom_id).copied()
.or_else(|| old_tree
.and_then(|t| t.dom_to_layout.get(&inline_dom_id))
.and_then(|v| v.first().copied()));
let reconciled_child_idx = reconcile_recursive(
styled_dom,
inline_dom_id,
old_child_idx,
Some(anon_idx), old_tree,
new_tree_builder,
recon,
debug_messages,
)?;
if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
new_child_hashes.push(child_node.subtree_hash.0);
}
}
children_are_different = true;
} }
let old_child_idx = old_children_by_dom.get(&new_child_dom_id).copied()
.or_else(|| old_children_indices.get(i).copied());
let reconciled_child_idx = reconcile_recursive(
styled_dom,
new_child_dom_id,
old_child_idx,
Some(new_node_idx),
old_tree,
new_tree_builder,
recon,
debug_messages,
)?;
if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
new_child_hashes.push(child_node.subtree_hash.0);
}
if old_tree.and_then(|t| t.cold(old_child_idx?).map(|n| n.subtree_hash))
!= new_tree_builder
.get(reconciled_child_idx)
.map(|n| n.subtree_hash)
{
children_are_different = true;
}
} else {
inline_run.push((i, new_child_dom_id));
}
}
if !inline_run.is_empty() {
if is_whitespace_only_inline_run(styled_dom, &inline_run, new_dom_id) {
if let Some(msgs) = debug_messages.as_mut() {
msgs.push(LayoutDebugMessage::info(format!(
"[reconcile_recursive] Skipping trailing whitespace-only inline run ({} nodes) in node {}",
inline_run.len(),
new_dom_id.index()
)));
}
} else {
let anon_idx = new_tree_builder.create_anonymous_node(
new_node_idx,
AnonymousBoxType::InlineWrapper,
FormattingContext::Inline, );
if let Some(msgs) = debug_messages.as_mut() {
msgs.push(LayoutDebugMessage::info(format!(
"[reconcile_recursive] Created trailing anonymous IFC wrapper (layout_idx={}) for {} inline children: {:?}",
anon_idx,
inline_run.len(),
inline_run.iter().map(|(_, id)| id.index()).collect::<Vec<_>>()
)));
}
#[allow(clippy::iter_with_drain)] for (pos, inline_dom_id) in inline_run.drain(..) {
let old_child_idx = old_children_by_dom.get(&inline_dom_id).copied();
let reconciled_child_idx = reconcile_recursive(
styled_dom,
inline_dom_id,
old_child_idx,
Some(anon_idx),
old_tree,
new_tree_builder,
recon,
debug_messages,
)?;
if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
new_child_hashes.push(child_node.subtree_hash.0);
}
}
children_are_different = true;
} }
}
let node_self_hash = {
use std::hash::{DefaultHasher, Hash, Hasher};
let mut h = DefaultHasher::new();
new_fingerprint.hash(&mut h);
h.finish()
};
let final_subtree_hash = calculate_subtree_hash(node_self_hash, &new_child_hashes);
if let Some(current_node) = new_tree_builder.get_mut(new_node_idx) {
current_node.subtree_hash = final_subtree_hash;
}
if dirty_flag >= DirtyFlag::Layout || children_are_different {
recon.intrinsic_dirty.insert(new_node_idx);
recon.layout_roots.insert(new_node_idx);
} else if dirty_flag == DirtyFlag::Paint {
recon.paint_dirty.insert(new_node_idx);
}
Ok(new_node_idx)
}
struct PreparedLayoutContext<'a> {
constraints: LayoutConstraints<'a>,
dom_id: Option<NodeId>,
writing_mode: LayoutWritingMode,
final_used_size: LogicalSize,
box_props: crate::solver3::geometry::BoxProps,
}
fn prepare_layout_context<'a, T: ParsedFontTrait>(
ctx: &LayoutContext<'a, T>,
tree: &LayoutTree,
node_index: usize,
containing_block_size: LogicalSize,
) -> Result<PreparedLayoutContext<'a>> {
let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
let warm = tree.warm(node_index).ok_or(LayoutError::InvalidTree)?;
let dom_id = node.dom_node_id;
let intrinsic = warm.intrinsic_sizes.unwrap_or_default();
let final_used_size = calculate_used_size_for_node(
ctx.styled_dom,
dom_id, &containing_block_size,
intrinsic,
&node.box_props.unpack(),
&ctx.viewport_size,
)?;
let writing_mode = warm.computed_style.writing_mode;
let text_align = warm.computed_style.text_align;
let display = warm.computed_style.display;
let overflow_y = warm.computed_style.overflow_y;
let height_is_auto = warm.computed_style.height.is_none();
let available_size_for_children = if height_is_auto {
let inner_size = node.box_props.inner_size(final_used_size, writing_mode);
let available_width = match display {
LayoutDisplay::Inline => containing_block_size.width,
_ => inner_size.width,
};
LogicalSize {
width: available_width,
height: containing_block_size.height,
}
} else {
node.box_props.inner_size(final_used_size, writing_mode)
};
let wm_ctx = crate::solver3::geometry::WritingModeContext::new(
writing_mode,
warm.computed_style.direction,
warm.computed_style.text_orientation,
);
let constraints = LayoutConstraints {
available_size: available_size_for_children,
bfc_state: None,
writing_mode,
writing_mode_ctx: wm_ctx,
text_align: style_text_align_to_fc(text_align),
containing_block_size,
available_width_type: Text3AvailableSpace::Definite(available_size_for_children.width),
};
Ok(PreparedLayoutContext {
constraints,
dom_id,
writing_mode,
final_used_size,
box_props: node.box_props.unpack(),
})
}
pub fn compute_scrollbar_info_core<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
dom_id: NodeId,
styled_node_state: &azul_core::styled_dom::StyledNodeState,
content_size: LogicalSize,
container_size: LogicalSize,
) -> ScrollbarRequirements {
if ctx.fragmentation_context.is_some() {
return ScrollbarRequirements::default();
}
let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, styled_node_state);
let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, styled_node_state);
let scrollbar_style = crate::solver3::getters::get_scrollbar_style_cached(
ctx, dom_id, styled_node_state,
);
let scrollbar_width_px = scrollbar_style.reserve_width_px;
let mut reqs = fc::check_scrollbar_necessity(
content_size,
container_size,
to_overflow_behavior(overflow_x),
to_overflow_behavior(overflow_y),
scrollbar_width_px,
);
reqs.visual_width_px = scrollbar_style.visual_width_px;
let scrollbar_gutter = get_scrollbar_gutter_property(ctx.styled_dom, dom_id, styled_node_state)
.unwrap_or(azul_css::props::layout::overflow::StyleScrollbarGutter::Auto);
let ob_y = to_overflow_behavior(overflow_y);
let is_scroll_container = matches!(ob_y, fc::OverflowBehavior::Scroll | fc::OverflowBehavior::Auto);
if is_scroll_container {
use azul_css::props::layout::overflow::StyleScrollbarGutter;
match scrollbar_gutter {
StyleScrollbarGutter::Stable => {
if !reqs.needs_vertical {
reqs.scrollbar_width = scrollbar_width_px;
}
}
StyleScrollbarGutter::StableBothEdges => {
reqs.scrollbar_width = scrollbar_width_px * 2.0;
}
StyleScrollbarGutter::Auto => {
}
}
}
reqs
}
fn compute_scrollbar_info<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
dom_id: NodeId,
styled_node_state: &azul_core::styled_dom::StyledNodeState,
content_size: LogicalSize,
box_props: &crate::solver3::geometry::BoxProps,
final_used_size: LogicalSize,
writing_mode: LayoutWritingMode,
) -> ScrollbarRequirements {
let container_size = box_props.inner_size(final_used_size, writing_mode);
compute_scrollbar_info_core(ctx, dom_id, styled_node_state, content_size, container_size)
}
fn check_scrollbar_change(
tree: &LayoutTree,
node_index: usize,
scrollbar_info: &ScrollbarRequirements,
skip_scrollbar_check: bool,
) -> bool {
if skip_scrollbar_check {
return false;
}
let Some(warm_node) = tree.warm(node_index) else {
return false;
};
warm_node.scrollbar_info.as_ref().map_or_else(|| scrollbar_info.needs_reflow(), |old_info| {
let horizontal_changed = old_info.needs_horizontal != scrollbar_info.needs_horizontal;
let vertical_changed = old_info.needs_vertical != scrollbar_info.needs_vertical;
horizontal_changed || vertical_changed
})
}
fn calculate_content_box_pos(
containing_block_pos: LogicalPosition,
box_props: &crate::solver3::geometry::BoxProps,
) -> LogicalPosition {
LogicalPosition::new(
containing_block_pos.x + box_props.border.left + box_props.padding.left,
containing_block_pos.y + box_props.border.top + box_props.padding.top,
)
}
fn log_content_box_calculation<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
node_index: usize,
current_node: &LayoutNodeHot,
containing_block_pos: LogicalPosition,
self_content_box_pos: LogicalPosition,
) {
let Some(debug_msgs) = ctx.debug_messages.as_mut() else {
return;
};
let dom_name = current_node
.dom_node_id
.and_then(|id| {
ctx.styled_dom
.node_data
.as_container()
.internal
.get(id.index())
}).map_or_else(|| "Unknown".to_string(), |n| format!("{:?}", n.node_type));
let cbp = current_node.box_props.unpack();
debug_msgs.push(LayoutDebugMessage::new(
LayoutDebugMessageType::PositionCalculation,
format!(
"[CONTENT BOX {}] {} - margin-box pos=({:.2}, {:.2}) + border=({:.2},{:.2}) + \
padding=({:.2},{:.2}) = content-box pos=({:.2}, {:.2})",
node_index,
dom_name,
containing_block_pos.x,
containing_block_pos.y,
cbp.border.left,
cbp.border.top,
cbp.padding.left,
cbp.padding.top,
self_content_box_pos.x,
self_content_box_pos.y
),
));
}
fn log_child_positioning<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
child_index: usize,
child_node: &LayoutNodeHot,
self_content_box_pos: LogicalPosition,
child_relative_pos: LogicalPosition,
child_absolute_pos: LogicalPosition,
) {
let child_dom_name = child_node
.dom_node_id
.and_then(|id| {
ctx.styled_dom
.node_data
.as_container()
.internal
.get(id.index())
}).map_or_else(|| "Unknown".to_string(), |n| format!("{:?}", n.node_type));
let Some(debug_msgs) = ctx.debug_messages.as_mut() else {
return;
};
debug_msgs.push(LayoutDebugMessage::new(
LayoutDebugMessageType::PositionCalculation,
format!(
"[CHILD POS {}] {} - parent content-box=({:.2}, {:.2}) + relative=({:.2}, {:.2}) + \
margin=({:.2}, {:.2}) = absolute=({:.2}, {:.2})",
child_index,
child_dom_name,
self_content_box_pos.x,
self_content_box_pos.y,
child_relative_pos.x,
child_relative_pos.y,
child_node.box_props.unpack().margin.left,
child_node.box_props.unpack().margin.top,
child_absolute_pos.x,
child_absolute_pos.y
),
));
}
fn process_inflow_child<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &TextLayoutCache,
child_index: usize,
child_relative_pos: LogicalPosition,
self_content_box_pos: LogicalPosition,
inner_size_after_scrollbars: LogicalSize,
writing_mode: LayoutWritingMode,
is_flex_or_grid: bool,
calculated_positions: &mut super::PositionVec,
reflow_needed_for_scrollbars: bool,
float_cache: &HashMap<usize, fc::FloatingContext>,
) -> Result<()> {
let child_warm = tree.warm_mut(child_index).ok_or(LayoutError::InvalidTree)?;
child_warm.relative_position = Some(child_relative_pos);
let child_absolute_pos = LogicalPosition::new(
self_content_box_pos.x + child_relative_pos.x,
self_content_box_pos.y + child_relative_pos.y,
);
{
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
log_child_positioning(
ctx,
child_index,
child_node,
self_content_box_pos,
child_relative_pos,
child_absolute_pos,
);
}
super::pos_set(calculated_positions, child_index, child_absolute_pos);
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let child_bp = child_node.box_props.unpack();
let child_content_box_pos =
calculate_content_box_pos(child_absolute_pos, &child_bp);
let child_inner_size = child_bp
.inner_size(child_node.used_size.unwrap_or_default(), writing_mode);
let child_children: Vec<usize> = tree.children(child_index).to_vec();
let child_fc = child_node.formatting_context;
if !child_children.is_empty() {
if is_flex_or_grid {
position_flex_child_descendants(
tree,
child_index,
child_content_box_pos,
child_inner_size,
calculated_positions,
)?;
} else {
position_bfc_child_descendants(
tree,
child_index,
child_content_box_pos,
calculated_positions,
);
}
}
Ok(())
}
fn position_bfc_child_descendants(
tree: &LayoutTree,
node_index: usize,
content_box_pos: LogicalPosition,
calculated_positions: &mut super::PositionVec,
) {
let Some(node) = tree.get(node_index) else { return };
for &child_index in tree.children(node_index) {
let Some(child_node) = tree.get(child_index) else { continue };
let child_rel_pos = tree.warm(child_index)
.and_then(|w| w.relative_position)
.unwrap_or_default();
let child_abs_pos = LogicalPosition::new(
content_box_pos.x + child_rel_pos.x,
content_box_pos.y + child_rel_pos.y,
);
super::pos_set(calculated_positions, child_index, child_abs_pos);
let cbp = child_node.box_props.unpack();
let child_content_box_pos = LogicalPosition::new(
child_abs_pos.x + cbp.border.left + cbp.padding.left,
child_abs_pos.y + cbp.border.top + cbp.padding.top,
);
position_bfc_child_descendants(tree, child_index, child_content_box_pos, calculated_positions);
}
}
fn process_out_of_flow_children<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut TextLayoutCache,
node_index: usize,
self_content_box_pos: LogicalPosition,
containing_block_size: LogicalSize,
calculated_positions: &mut super::PositionVec,
reflow_needed_for_scrollbars: &mut bool,
float_cache: &mut HashMap<usize, fc::FloatingContext>,
) -> Result<()> {
let out_of_flow_children: Vec<(usize, Option<NodeId>)> = {
let current_node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
tree.children(node_index)
.iter()
.filter_map(|&child_index| {
if super::pos_contains(calculated_positions, child_index) {
return None;
}
let child = tree.get(child_index)?;
Some((child_index, child.dom_node_id))
})
.collect()
};
for (child_index, child_dom_id_opt) in out_of_flow_children {
let Some(child_dom_id) = child_dom_id_opt else {
continue;
};
let position_type = get_position_type(ctx.styled_dom, Some(child_dom_id));
if position_type != LayoutPosition::Absolute && position_type != LayoutPosition::Fixed {
continue;
}
super::pos_set(calculated_positions, child_index, self_content_box_pos);
calculate_layout_for_subtree(
ctx,
tree,
text_cache,
child_index,
self_content_box_pos,
containing_block_size,
calculated_positions,
reflow_needed_for_scrollbars,
float_cache,
ComputeMode::PerformLayout,
)?;
}
Ok(())
}
#[allow(clippy::implicit_hasher)] #[allow(clippy::too_many_lines)] pub fn calculate_layout_for_subtree<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut TextLayoutCache,
node_index: usize,
containing_block_pos: LogicalPosition,
containing_block_size: LogicalSize,
calculated_positions: &mut super::PositionVec,
reflow_needed_for_scrollbars: &mut bool,
float_cache: &mut HashMap<usize, fc::FloatingContext>,
compute_mode: ComputeMode,
) -> Result<()> {
#[cfg(feature = "web_lift")]
unsafe {
let m = match compute_mode { ComputeMode::PerformLayout => 0xC0DE0002u32, _ => 0xC0DE0001u32 };
crate::az_mark(((0x60980 + (node_index & 7) * 4)) as u32, (m) as u32);
}
let _probe = match compute_mode {
ComputeMode::ComputeSize => crate::probe::Probe::span("size_node"),
ComputeMode::PerformLayout => crate::probe::Probe::span("pos_node"),
};
if node_index < ctx.cache_map.entries.len() {
match compute_mode {
ComputeMode::ComputeSize => {
let sizing_hit = ctx.cache_map.entries[node_index]
.get_size(0, containing_block_size)
.copied();
if let Some(cached_sizing) = sizing_hit {
drop(crate::probe::Probe::span("size_cache_hit_sizing"));
if let Some(node) = tree.get_mut(node_index) {
node.used_size = Some(cached_sizing.result_size);
}
if let Some(warm) = tree.warm_mut(node_index) {
warm.escaped_top_margin = cached_sizing.escaped_top_margin;
warm.escaped_bottom_margin = cached_sizing.escaped_bottom_margin;
warm.baseline = cached_sizing.baseline;
}
return Ok(());
}
let layout_hit = ctx.cache_map.entries[node_index]
.get_layout(containing_block_size)
.cloned();
if let Some(cached_layout) = layout_hit {
drop(crate::probe::Probe::span("size_cache_hit_layout"));
if let Some(node) = tree.get_mut(node_index) {
node.used_size = Some(cached_layout.result_size);
}
if let Some(warm) = tree.warm_mut(node_index) {
warm.overflow_content_size = Some(cached_layout.content_size);
warm.scrollbar_info = Some(cached_layout.scrollbar_info);
}
return Ok(());
}
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark(((0x60A60 + (node_index & 7) * 4)) as u32, (0xC0DE0001) as u32); }
drop(crate::probe::Probe::span("size_cache_miss"));
}
ComputeMode::PerformLayout => {
let layout_hit = ctx.cache_map.entries[node_index]
.get_layout(containing_block_size)
.cloned();
if let Some(cached_layout) = layout_hit {
drop(crate::probe::Probe::span("pos_cache_hit"));
if let Some(node) = tree.get_mut(node_index) {
node.used_size = Some(cached_layout.result_size);
}
if let Some(warm) = tree.warm_mut(node_index) {
warm.overflow_content_size = Some(cached_layout.content_size);
warm.scrollbar_info = Some(cached_layout.scrollbar_info);
}
let box_props = tree.get(node_index)
.map(|n| n.box_props.unpack())
.unwrap_or_default();
let writing_mode = tree
.warm(node_index)
.map(|w| w.computed_style.writing_mode)
.unwrap_or_default();
let self_content_box_pos = calculate_content_box_pos(containing_block_pos, &box_props);
let result_size = cached_layout.result_size;
for (child_index, child_relative_pos) in &cached_layout.child_positions {
let child_abs_pos = LogicalPosition::new(
self_content_box_pos.x + child_relative_pos.x,
self_content_box_pos.y + child_relative_pos.y,
);
super::pos_set(calculated_positions, *child_index, child_abs_pos);
let inner = box_props.inner_size(
result_size,
writing_mode,
);
let child_available_size =
cached_layout.scrollbar_info.shrink_size(inner);
calculate_layout_for_subtree(
ctx,
tree,
text_cache,
*child_index,
child_abs_pos,
child_available_size,
calculated_positions,
reflow_needed_for_scrollbars,
float_cache,
compute_mode,
)?;
}
return Ok(());
}
}
}
}
if compute_mode == ComputeMode::PerformLayout {
drop(crate::probe::Probe::span("pos_cache_miss"));
}
let PreparedLayoutContext {
constraints,
dom_id,
writing_mode,
mut final_used_size,
box_props,
} = {
let _p = crate::probe::Probe::span("prepare_layout_context");
prepare_layout_context(ctx, tree, node_index, containing_block_size)?
};
{
let is_table_cell = tree.get(node_index).is_some_and(|n| {
matches!(n.formatting_context, FormattingContext::TableCell)
});
if !is_table_cell {
if let Some(node) = tree.get_mut(node_index) {
node.used_size = Some(final_used_size);
}
}
}
let layout_result = {
let _p = crate::probe::Probe::span("layout_formatting_context");
layout_formatting_context(ctx, tree, text_cache, node_index, &constraints, float_cache)?
};
let content_size = layout_result.output.overflow_size;
if let Some(adjusted) = tree.get(node_index).and_then(|n| n.used_size) {
final_used_size = adjusted;
}
let styled_node_state = dom_id
.and_then(|id| ctx.styled_dom.styled_nodes.as_container().get(id).cloned())
.map(|n| n.styled_node_state)
.unwrap_or_default();
let css_height: MultiValue<LayoutHeight> = match dom_id {
Some(id) => get_css_height(ctx.styled_dom, id, &styled_node_state),
None => MultiValue::Auto, };
let is_scroll_container = dom_id.is_some_and(|id| {
let ov_x = get_overflow_x(ctx.styled_dom, id, &styled_node_state);
let ov_y = get_overflow_y(ctx.styled_dom, id, &styled_node_state);
matches!(ov_x, MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto))
|| matches!(ov_y, MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto))
});
if should_use_content_height(&css_height) {
let skip_expansion = is_scroll_container
&& containing_block_size.height.is_finite()
&& containing_block_size.height > 0.0;
if !skip_expansion {
final_used_size = apply_content_based_height(
final_used_size,
content_size,
tree,
node_index,
writing_mode,
)?;
}
}
let skip_scrollbar_check = ctx.fragmentation_context.is_some();
let scrollbar_info = dom_id.map_or_else(ScrollbarRequirements::default, |id| {
compute_scrollbar_info(
ctx,
id,
&styled_node_state,
content_size,
&box_props,
final_used_size,
writing_mode,
)
});
if check_scrollbar_change(tree, node_index, &scrollbar_info, skip_scrollbar_check) {
*reflow_needed_for_scrollbars = true;
}
let merged_scrollbar_info = scrollbar_info;
let content_box_size = box_props.inner_size(final_used_size, writing_mode);
let inner_size_after_scrollbars = merged_scrollbar_info.shrink_size(content_box_size);
let self_content_box_pos = {
{
let current_node = tree.get_mut(node_index).ok_or(LayoutError::InvalidTree)?;
let is_table_cell = matches!(
current_node.formatting_context,
FormattingContext::TableCell
);
if !is_table_cell || current_node.used_size.is_none() {
current_node.used_size = Some(final_used_size);
}
}
if let Some(warm) = tree.warm_mut(node_index) {
warm.scrollbar_info = Some(merged_scrollbar_info);
warm.overflow_content_size = Some(content_size);
}
let current_node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
let current_bp = current_node.box_props.unpack();
let pos = calculate_content_box_pos(containing_block_pos, ¤t_bp);
log_content_box_calculation(ctx, node_index, current_node, containing_block_pos, pos);
pos
};
let is_flex_or_grid = {
let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
matches!(
node.formatting_context,
FormattingContext::Flex | FormattingContext::Grid
)
};
let positions: Vec<_> = layout_result
.output
.positions
.iter()
.map(|(&idx, &pos)| (idx, pos))
.collect();
let child_positions_for_cache: Vec<(usize, LogicalPosition)> = positions.clone();
for (child_index, child_relative_pos) in positions {
process_inflow_child(
ctx,
tree,
text_cache,
child_index,
child_relative_pos,
self_content_box_pos,
inner_size_after_scrollbars,
writing_mode,
is_flex_or_grid,
calculated_positions,
*reflow_needed_for_scrollbars,
float_cache,
)?;
}
process_out_of_flow_children(
ctx,
tree,
text_cache,
node_index,
self_content_box_pos,
inner_size_after_scrollbars,
calculated_positions,
reflow_needed_for_scrollbars,
float_cache,
)?;
if node_index < ctx.cache_map.entries.len() {
let warm_ref = tree.warm(node_index);
let baseline = warm_ref.and_then(|n| n.baseline);
let escaped_top = warm_ref.and_then(|n| n.escaped_top_margin);
let escaped_bottom = warm_ref.and_then(|n| n.escaped_bottom_margin);
ctx.cache_map.get_mut(node_index).store_layout(LayoutCacheEntry {
available_size: containing_block_size,
result_size: final_used_size,
content_size,
child_positions: child_positions_for_cache,
escaped_top_margin: escaped_top,
escaped_bottom_margin: escaped_bottom,
scrollbar_info: merged_scrollbar_info,
});
ctx.cache_map.get_mut(node_index).store_size(0, SizingCacheEntry {
available_size: containing_block_size,
result_size: final_used_size,
baseline,
escaped_top_margin: escaped_top,
escaped_bottom_margin: escaped_bottom,
});
}
Ok(())
}
fn position_flex_child_descendants(
tree: &mut LayoutTree,
node_index: usize,
content_box_pos: LogicalPosition,
available_size: LogicalSize,
calculated_positions: &mut super::PositionVec,
) -> Result<()> {
let children: Vec<usize> = tree.children(node_index).to_vec();
for &child_index in &children {
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let child_rel_pos = tree.warm(child_index)
.and_then(|w| w.relative_position)
.unwrap_or_default();
let child_abs_pos = LogicalPosition::new(
content_box_pos.x + child_rel_pos.x,
content_box_pos.y + child_rel_pos.y,
);
super::pos_set(calculated_positions, child_index, child_abs_pos);
let cbp = child_node.box_props.unpack();
let child_writing_mode = tree
.warm(child_index)
.map(|w| w.computed_style.writing_mode)
.unwrap_or_default();
let child_content_box = LogicalPosition::new(
child_abs_pos.x
+ cbp.border.left
+ cbp.padding.left,
child_abs_pos.y
+ cbp.border.top
+ cbp.padding.top,
);
let child_inner_size = cbp.inner_size(
child_node.used_size.unwrap_or_default(),
child_writing_mode,
);
position_flex_child_descendants(
tree,
child_index,
child_content_box,
child_inner_size,
calculated_positions,
)?;
}
Ok(())
}
#[allow(clippy::match_same_arms)] fn should_use_content_height(css_height: &MultiValue<LayoutHeight>) -> bool {
match css_height {
MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
true
}
MultiValue::Exact(height) => match height {
LayoutHeight::Auto => {
true
}
LayoutHeight::Px(px) => {
use azul_css::props::basic::{pixel::PixelValue, SizeMetric};
px == &PixelValue::zero()
|| (px.metric != SizeMetric::Px
&& px.metric != SizeMetric::Percent
&& px.metric != SizeMetric::Em
&& px.metric != SizeMetric::Rem)
}
LayoutHeight::MinContent | LayoutHeight::MaxContent | LayoutHeight::FitContent(_) => {
true
}
LayoutHeight::Calc(_) => {
false
}
},
}
}
fn apply_content_based_height(
mut used_size: LogicalSize,
content_size: LogicalSize,
tree: &LayoutTree,
node_index: usize,
writing_mode: LayoutWritingMode,
) -> Result<LogicalSize> {
let node_props = tree.get(node_index).ok_or(LayoutError::InvalidTree)?.box_props.unpack();
let main_axis_padding_border =
node_props.padding.main_sum(writing_mode) + node_props.border.main_sum(writing_mode);
let old_main_size = used_size.main(writing_mode);
let new_main_size = content_size.main(writing_mode) + main_axis_padding_border;
let final_main_size = old_main_size.max(new_main_size);
used_size = used_size.with_main(writing_mode, final_main_size);
Ok(used_size)
}
fn calculate_subtree_hash(node_self_hash: u64, child_hashes: &[u64]) -> SubtreeHash {
let mut hasher = DefaultHasher::new();
node_self_hash.hash(&mut hasher);
child_hashes.hash(&mut hasher);
SubtreeHash(hasher.finish())
}
#[allow(clippy::implicit_hasher)] pub fn compute_counters(
styled_dom: &StyledDom,
tree: &LayoutTree,
counters: &mut HashMap<(usize, String), i32>,
) {
let mut counter_stacks: HashMap<String, Vec<i32>> = HashMap::new();
let mut scope_stack: Vec<Vec<String>> = Vec::new();
compute_counters_recursive(
styled_dom,
tree,
tree.root,
counters,
&mut counter_stacks,
&mut scope_stack,
);
}
#[allow(clippy::too_many_lines)] fn compute_counters_recursive(
styled_dom: &StyledDom,
tree: &LayoutTree,
node_idx: usize,
counters: &mut HashMap<(usize, String), i32>,
counter_stacks: &mut HashMap<String, Vec<i32>>,
scope_stack: &mut Vec<Vec<String>>,
) {
let Some(node) = tree.get(node_idx) else {
return;
};
if tree.warm(node_idx).and_then(|w| w.pseudo_element.as_ref()).is_some() {
if let Some(parent_idx) = node.parent {
let parent_counters: Vec<_> = counters
.iter()
.filter(|((idx, _), _)| *idx == parent_idx)
.map(|((_, name), &value)| (name.clone(), value))
.collect();
for (counter_name, value) in parent_counters {
counters.insert((node_idx, counter_name), value);
}
}
return;
}
let Some(dom_id) = node.dom_node_id else {
for &child_idx in tree.children(node_idx) {
compute_counters_recursive(
styled_dom,
tree,
child_idx,
counters,
counter_stacks,
scope_stack,
);
}
return;
};
let node_data = &styled_dom.node_data.as_container()[dom_id];
let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
let cache = &styled_dom.css_property_cache.ptr;
let mut reset_counters_at_this_level = Vec::new();
let display = {
use crate::solver3::getters::get_display_property;
get_display_property(styled_dom, Some(dom_id)).exact()
};
let is_list_item = matches!(display, Some(LayoutDisplay::ListItem));
let has_counter_css = node_state.is_normal()
&& cache.compact_cache.as_ref().is_none_or(|cc| cc.has_counter(dom_id.index()));
let counter_reset = if has_counter_css {
cache
.get_counter_reset(node_data, &dom_id, node_state)
.and_then(|v| v.get_property())
} else {
None
};
if let Some(counter_reset) = counter_reset {
let counter_name_str = counter_reset.counter_name.as_str();
if counter_name_str != "none" {
let counter_name = counter_name_str.to_string();
let reset_value = counter_reset.value;
counter_stacks
.entry(counter_name.clone())
.or_default()
.push(reset_value);
reset_counters_at_this_level.push(counter_name);
}
}
let counter_inc = if has_counter_css {
cache
.get_counter_increment(node_data, &dom_id, node_state)
.and_then(|v| v.get_property())
} else {
None
};
if let Some(counter_inc) = counter_inc {
let counter_name_str = counter_inc.counter_name.as_str();
if counter_name_str != "none" {
let counter_name = counter_name_str.to_string();
let inc_value = counter_inc.value;
let stack = counter_stacks.entry(counter_name).or_default();
if stack.is_empty() {
stack.push(inc_value);
} else if let Some(current) = stack.last_mut() {
*current += inc_value;
}
}
}
if is_list_item {
let counter_name = "list-item".to_string();
let stack = counter_stacks.entry(counter_name).or_default();
if stack.is_empty() {
stack.push(1);
} else if let Some(current) = stack.last_mut() {
*current += 1;
}
}
for (counter_name, stack) in counter_stacks.iter() {
if let Some(&value) = stack.last() {
counters.insert((node_idx, counter_name.clone()), value);
}
}
scope_stack.push(reset_counters_at_this_level.clone());
for &child_idx in tree.children(node_idx) {
compute_counters_recursive(
styled_dom,
tree,
child_idx,
counters,
counter_stacks,
scope_stack,
);
}
if let Some(reset_counters) = scope_stack.pop() {
for counter_name in reset_counters {
if let Some(stack) = counter_stacks.get_mut(&counter_name) {
stack.pop();
}
}
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod autotest_generated {
use azul_core::dom::{Dom, IdOrClass};
use azul_css::props::{
basic::{pixel::PixelValue, SizeMetric},
layout::dimensions::CalcAstItemVec,
};
use super::*;
use crate::solver3::{
display_list::DisplayList,
geometry::{EdgeSizes, MarginAuto, PackedBoxProps, ResolvedBoxProps},
layout_tree::{LayoutNodeCold, LayoutNodeWarm},
pos_get, PositionVec, POSITION_UNSET,
};
fn size(w: f32, h: f32) -> LogicalSize {
LogicalSize::new(w, h)
}
fn pos(x: f32, y: f32) -> LogicalPosition {
LogicalPosition::new(x, y)
}
fn sizing_entry(available: LogicalSize, result: LogicalSize) -> SizingCacheEntry {
SizingCacheEntry {
available_size: available,
result_size: result,
baseline: None,
escaped_top_margin: None,
escaped_bottom_margin: None,
}
}
fn layout_entry(available: LogicalSize, result: LogicalSize) -> LayoutCacheEntry {
LayoutCacheEntry {
available_size: available,
result_size: result,
content_size: result,
child_positions: Vec::new(),
escaped_top_margin: None,
escaped_bottom_margin: None,
scrollbar_info: ScrollbarRequirements::default(),
}
}
fn edges(top: f32, right: f32, bottom: f32, left: f32) -> EdgeSizes {
EdgeSizes {
top,
right,
bottom,
left,
}
}
fn box_props(margin: EdgeSizes, border: EdgeSizes, padding: EdgeSizes) -> ResolvedBoxProps {
ResolvedBoxProps {
margin,
padding,
border,
margin_auto: MarginAuto::default(),
}
}
fn zero_box_props() -> ResolvedBoxProps {
box_props(
edges(0.0, 0.0, 0.0, 0.0),
edges(0.0, 0.0, 0.0, 0.0),
edges(0.0, 0.0, 0.0, 0.0),
)
}
fn hot(
parent: Option<usize>,
dom_node_id: Option<NodeId>,
used_size: Option<LogicalSize>,
bp: &ResolvedBoxProps,
) -> LayoutNodeHot {
LayoutNodeHot {
box_props: PackedBoxProps::pack(bp),
dom_node_id,
used_size,
formatting_context: FormattingContext::Block {
establishes_new_context: false,
},
parent,
}
}
fn plain(parent: Option<usize>) -> LayoutNodeHot {
hot(parent, None, Some(size(0.0, 0.0)), &zero_box_props())
}
fn build_tree(
nodes: Vec<LayoutNodeHot>,
warm: Vec<LayoutNodeWarm>,
child_lists: &[Vec<usize>],
) -> LayoutTree {
let n = nodes.len();
let mut children_arena: Vec<usize> = Vec::new();
let mut children_offsets: Vec<(u32, u32)> = Vec::with_capacity(n);
for cl in child_lists {
let start = u32::try_from(children_arena.len()).unwrap();
children_arena.extend_from_slice(cl);
children_offsets.push((start, u32::try_from(cl.len()).unwrap()));
}
while children_offsets.len() < n {
children_offsets.push((0, 0));
}
LayoutTree {
nodes,
warm,
cold: vec![LayoutNodeCold::default(); n],
root: 0,
dom_to_layout: BTreeMap::new(),
children_arena,
children_offsets,
subtree_needs_intrinsic: Vec::new(),
}
}
fn warm_default(n: usize) -> Vec<LayoutNodeWarm> {
vec![LayoutNodeWarm::default(); n]
}
fn div_class(class: &str) -> Dom {
Dom::create_div().with_ids_and_classes(vec![IdOrClass::Class(class.into())].into())
}
fn styled(dom: Dom, css_str: &str) -> StyledDom {
let mut dom = dom;
let (css, _warnings) = azul_css::parser2::new_from_str(css_str);
StyledDom::create(&mut dom, css)
}
fn whitespace_dom(css_str: &str) -> StyledDom {
styled(
Dom::create_body().with_child(
div_class("p")
.with_child(Dom::create_text(" \n\t"))
.with_child(Dom::create_text("hi"))
.with_child(Dom::create_div())
.with_child(Dom::create_text("\u{00A0}")),
),
css_str,
)
}
#[test]
fn nodecache_default_is_empty_and_never_hits() {
let c = NodeCache::default();
assert!(c.is_empty);
assert!(c.layout_entry.is_none());
assert!(c.measure_entries.iter().all(Option::is_none));
for slot in 0..9 {
assert!(c.get_size(slot, size(0.0, 0.0)).is_none());
assert!(c.get_size(slot, size(100.0, 100.0)).is_none());
}
assert!(c.get_layout(size(0.0, 0.0)).is_none());
}
#[test]
fn nodecache_store_size_then_exact_lookup_round_trips() {
let mut c = NodeCache::default();
c.store_size(0, sizing_entry(size(200.0, 100.0), size(50.0, 40.0)));
assert!(!c.is_empty);
let hit = c.get_size(0, size(200.0, 100.0)).expect("exact hit");
assert_eq!(hit.available_size, size(200.0, 100.0));
assert_eq!(hit.result_size, size(50.0, 40.0));
}
#[test]
fn nodecache_get_size_result_matches_request() {
let mut c = NodeCache::default();
c.store_size(0, sizing_entry(size(200.0, 100.0), size(50.0, 40.0)));
let hit = c.get_size(0, size(50.0, 40.0)).expect("result-matches-request hit");
assert_eq!(hit.result_size, size(50.0, 40.0));
assert!(c.get_size(0, size(51.0, 41.0)).is_none());
}
#[test]
fn nodecache_get_size_epsilon_boundary() {
let mut c = NodeCache::default();
c.store_size(0, sizing_entry(size(100.0, 100.0), size(10.0, 10.0)));
assert!(c.get_size(0, size(100.05, 99.95)).is_some());
assert!(c.get_size(0, size(100.2, 100.0)).is_none());
assert!(c.get_size(0, size(100.0, 99.5)).is_none());
}
#[test]
fn nodecache_get_size_nan_request_misses_instead_of_panicking() {
let mut c = NodeCache::default();
c.store_size(0, sizing_entry(size(100.0, 100.0), size(10.0, 10.0)));
assert!(c.get_size(0, size(f32::NAN, 100.0)).is_none());
assert!(c.get_size(0, size(100.0, f32::NAN)).is_none());
assert!(c.get_size(0, size(f32::NAN, f32::NAN)).is_none());
}
#[test]
fn nodecache_nan_and_infinite_entries_are_unreachable() {
let mut c = NodeCache::default();
c.store_size(
0,
sizing_entry(size(f32::INFINITY, f32::INFINITY), size(f32::INFINITY, f32::INFINITY)),
);
assert!(c.get_size(0, size(f32::INFINITY, f32::INFINITY)).is_none());
c.store_size(1, sizing_entry(size(f32::NAN, f32::NAN), size(f32::NAN, f32::NAN)));
assert!(c.get_size(1, size(f32::NAN, f32::NAN)).is_none());
assert!(!c.is_empty);
}
#[test]
fn nodecache_handles_zero_and_negative_sizes() {
let mut c = NodeCache::default();
c.store_size(0, sizing_entry(size(0.0, 0.0), size(0.0, 0.0)));
assert!(c.get_size(0, size(0.0, 0.0)).is_some());
assert!(c.get_size(0, size(-0.0, -0.0)).is_some());
c.store_size(1, sizing_entry(size(-100.0, -50.0), size(-1.0, -1.0)));
let hit = c.get_size(1, size(-100.0, -50.0)).expect("negative sizes are deterministic");
assert_eq!(hit.result_size, size(-1.0, -1.0));
}
#[test]
fn nodecache_extreme_finite_sizes_do_not_panic() {
let mut c = NodeCache::default();
c.store_size(0, sizing_entry(size(f32::MAX, f32::MIN), size(f32::MAX, f32::MIN)));
assert!(c.get_size(0, size(f32::MAX, f32::MIN)).is_some());
assert!(c.get_size(0, size(f32::MIN, f32::MAX)).is_none());
}
#[test]
fn nodecache_slots_are_independent() {
let mut c = NodeCache::default();
c.store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
c.store_size(8, sizing_entry(size(2.0, 2.0), size(2.0, 2.0)));
assert!(c.get_size(0, size(1.0, 1.0)).is_some());
assert!(c.get_size(8, size(2.0, 2.0)).is_some());
assert!(c.get_size(0, size(2.0, 2.0)).is_none());
assert!(c.get_size(8, size(1.0, 1.0)).is_none());
assert!(c.get_size(4, size(1.0, 1.0)).is_none());
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn nodecache_get_size_slot_out_of_range_panics() {
let c = NodeCache::default();
let _ = c.get_size(9, size(0.0, 0.0));
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn nodecache_store_size_slot_out_of_range_panics() {
let mut c = NodeCache::default();
c.store_size(9, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
}
#[test]
fn nodecache_layout_slot_round_trips_and_matches_result() {
let mut c = NodeCache::default();
c.store_layout(layout_entry(size(800.0, 600.0), size(800.0, 123.0)));
assert!(!c.is_empty);
assert!(c.get_layout(size(800.0, 600.0)).is_some());
let hit = c.get_layout(size(800.0, 123.0)).expect("result-matches-request");
assert_eq!(hit.result_size, size(800.0, 123.0));
assert!(c.get_layout(size(400.0, 300.0)).is_none());
assert!(c.get_layout(size(f32::NAN, f32::NAN)).is_none());
}
#[test]
fn nodecache_clear_wipes_every_slot() {
let mut c = NodeCache::default();
for slot in 0..9 {
c.store_size(slot, sizing_entry(size(10.0, 10.0), size(10.0, 10.0)));
}
c.store_layout(layout_entry(size(10.0, 10.0), size(10.0, 10.0)));
assert!(!c.is_empty);
c.clear();
assert!(c.is_empty);
assert!(c.layout_entry.is_none());
assert!(c.measure_entries.iter().all(Option::is_none));
assert!(c.get_size(0, size(10.0, 10.0)).is_none());
assert!(c.get_layout(size(10.0, 10.0)).is_none());
c.clear();
assert!(c.is_empty);
}
#[test]
fn slot_index_always_lands_in_range_and_partitions_the_unknown_case() {
use AvailableWidthType::{Definite, MaxContent, MinContent};
let types = [Definite, MinContent, MaxContent];
for &wt in &types {
for &ht in &types {
for wk in [true, false] {
for hk in [true, false] {
let slot = NodeCache::slot_index(wk, hk, wt, ht);
assert!(slot < 9, "slot {slot} out of the 9-slot range");
}
}
}
}
for &wt in &types {
for &ht in &types {
assert_eq!(NodeCache::slot_index(true, true, wt, ht), 0);
}
}
let mut neither: Vec<usize> = Vec::new();
for &wt in &[Definite, MinContent] {
for &ht in &[Definite, MinContent] {
neither.push(NodeCache::slot_index(false, false, wt, ht));
}
}
neither.sort_unstable();
assert_eq!(neither, vec![5, 6, 7, 8]);
}
#[test]
fn slot_index_collapses_definite_and_maxcontent_onto_one_slot() {
use AvailableWidthType::{Definite, MaxContent, MinContent};
assert_eq!(
NodeCache::slot_index(true, false, Definite, Definite),
NodeCache::slot_index(true, false, MaxContent, Definite)
);
assert_ne!(
NodeCache::slot_index(true, false, Definite, Definite),
NodeCache::slot_index(true, false, MinContent, Definite)
);
}
#[test]
fn slot_index_keys_the_single_unknown_axis_off_the_known_axis_type() {
use AvailableWidthType::{Definite, MinContent};
assert_eq!(NodeCache::slot_index(true, false, Definite, MinContent), 1);
assert_eq!(NodeCache::slot_index(true, false, MinContent, Definite), 2);
assert_eq!(NodeCache::slot_index(false, true, MinContent, Definite), 3);
assert_eq!(NodeCache::slot_index(false, true, Definite, MinContent), 4);
}
#[test]
fn cachemap_resize_to_tree_grows_shrinks_and_zeroes() {
let mut m = LayoutCacheMap::default();
assert!(m.entries.is_empty());
m.resize_to_tree(0);
assert!(m.entries.is_empty());
m.resize_to_tree(3);
assert_eq!(m.entries.len(), 3);
assert!(m.entries.iter().all(|e| e.is_empty));
m.get_mut(1).store_size(0, sizing_entry(size(5.0, 5.0), size(5.0, 5.0)));
m.resize_to_tree(5);
assert_eq!(m.entries.len(), 5);
assert!(!m.get(1).is_empty);
assert!(m.get(4).is_empty);
m.resize_to_tree(1);
assert_eq!(m.entries.len(), 1);
m.resize_to_tree(0);
assert!(m.entries.is_empty());
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn cachemap_get_out_of_range_panics() {
let m = LayoutCacheMap::default();
let _ = m.get(0);
}
#[test]
fn cachemap_mark_dirty_out_of_range_index_is_a_noop() {
let mut m = LayoutCacheMap::default();
m.resize_to_tree(2);
m.get_mut(0).store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
m.mark_dirty(99, &[]);
m.mark_dirty(usize::MAX, &[]);
assert!(!m.get(0).is_empty);
assert_eq!(m.entries.len(), 2);
}
#[test]
fn cachemap_mark_dirty_propagates_up_the_ancestor_chain() {
let tree = vec![plain(None), plain(Some(0)), plain(Some(1))];
let mut m = LayoutCacheMap::default();
m.resize_to_tree(3);
for i in 0..3 {
m.get_mut(i).store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
}
m.mark_dirty(2, &tree);
assert!(m.get(2).is_empty);
assert!(m.get(1).is_empty);
assert!(m.get(0).is_empty);
}
#[test]
fn cachemap_mark_dirty_stops_at_the_first_dirty_ancestor() {
let tree = vec![plain(None), plain(Some(0)), plain(Some(1))];
let mut m = LayoutCacheMap::default();
m.resize_to_tree(3);
m.get_mut(0).store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
m.get_mut(2).store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
m.mark_dirty(2, &tree);
assert!(m.get(2).is_empty);
assert!(m.get(1).is_empty);
assert!(!m.get(0).is_empty);
}
#[test]
fn cachemap_mark_dirty_on_an_already_dirty_node_leaves_ancestors_alone() {
let tree = vec![plain(None), plain(Some(0))];
let mut m = LayoutCacheMap::default();
m.resize_to_tree(2);
m.get_mut(0).store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
m.mark_dirty(1, &tree);
assert!(m.get(1).is_empty);
assert!(!m.get(0).is_empty);
}
#[test]
fn cachemap_mark_dirty_terminates_on_cyclic_parent_links() {
let tree = vec![plain(Some(1)), plain(Some(0)), plain(Some(2))];
let mut m = LayoutCacheMap::default();
m.resize_to_tree(3);
for i in 0..3 {
m.get_mut(i).store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
}
m.mark_dirty(0, &tree);
assert!(m.get(0).is_empty);
assert!(m.get(1).is_empty);
m.mark_dirty(2, &tree);
assert!(m.get(2).is_empty);
}
#[test]
fn cachemap_mark_dirty_survives_a_tree_that_disagrees_with_the_cache() {
let mut m = LayoutCacheMap::default();
m.resize_to_tree(3);
for i in 0..3 {
m.get_mut(i).store_size(0, sizing_entry(size(1.0, 1.0), size(1.0, 1.0)));
}
let short_tree = vec![plain(None)];
m.mark_dirty(2, &short_tree);
assert!(m.get(2).is_empty);
assert!(!m.get(0).is_empty);
let dangling = vec![plain(Some(usize::MAX)), plain(None), plain(None)];
m.mark_dirty(0, &dangling);
assert!(m.get(0).is_empty);
}
#[test]
fn memory_report_total_bytes_sums_every_field_exactly_once() {
assert_eq!(Solver3CacheMemoryReport::default().total_bytes(), 0);
let r = Solver3CacheMemoryReport {
tree_bytes: 1,
tree_report: None,
calculated_positions_bytes: 2,
previous_positions_bytes: 4,
scroll_ids_bytes: 8,
scroll_id_to_node_id_bytes: 16,
counters_bytes: 32,
float_cache_bytes: 64,
cache_map_bytes: 128,
cached_display_list_bytes: 256,
};
assert_eq!(r.total_bytes(), 511);
}
#[test]
fn memory_report_of_a_default_cache_is_all_zero() {
let cache = LayoutCache::default();
let r = cache.memory_report();
assert_eq!(r.total_bytes(), 0);
assert_eq!(r.tree_bytes, 0);
assert!(r.tree_report.is_none());
assert_eq!(r.cache_map_bytes, 0);
assert_eq!(r.cached_display_list_bytes, 0);
}
#[test]
fn memory_report_accounts_for_populated_state() {
let mut cache = LayoutCache::default();
cache.cache_map.resize_to_tree(4);
cache.calculated_positions = vec![pos(1.0, 2.0), pos(3.0, 4.0), pos(5.0, 6.0)];
cache.previous_positions = vec![pos(0.0, 0.0)];
cache.counters.insert((0, "list-item".to_string()), 7);
cache.float_cache.insert(0, fc::FloatingContext::default());
cache.scroll_ids.insert(0, 42);
cache.scroll_id_to_node_id.insert(42, NodeId::ZERO);
cache.cached_display_list = Some((
SubtreeHash(1),
LogicalRect::new(pos(0.0, 0.0), size(10.0, 10.0)),
DisplayList::default(),
));
let r = cache.memory_report();
assert!(r.cache_map_bytes >= 4 * size_of::<NodeCache>());
assert_eq!(r.calculated_positions_bytes, 3 * size_of::<LogicalPosition>());
assert_eq!(r.previous_positions_bytes, size_of::<LogicalPosition>());
assert!(r.counters_bytes >= "list-item".len());
assert_eq!(r.float_cache_bytes, 256);
assert_eq!(r.cached_display_list_bytes, 2048);
assert_eq!(
r.total_bytes(),
r.tree_bytes
+ r.calculated_positions_bytes
+ r.previous_positions_bytes
+ r.scroll_ids_bytes
+ r.scroll_id_to_node_id_bytes
+ r.counters_bytes
+ r.float_cache_bytes
+ r.cache_map_bytes
+ r.cached_display_list_bytes
);
}
#[test]
fn reset_incremental_drops_reuse_state_keeps_the_rest_and_is_idempotent() {
let mut cache = LayoutCache {
tree: Some(build_tree(vec![plain(None)], warm_default(1), &[vec![]])),
..Default::default()
};
cache.cache_map.resize_to_tree(2);
cache.cached_display_list = Some((
SubtreeHash(9),
LogicalRect::new(pos(0.0, 0.0), size(1.0, 1.0)),
DisplayList::default(),
));
cache.prev_dom_ptr = 0xDEAD_BEEF;
cache.counters.insert((0, "c".to_string()), 1);
cache.float_cache.insert(0, fc::FloatingContext::default());
cache.calculated_positions = vec![pos(1.0, 2.0)];
cache.scroll_ids.insert(0, 5);
cache.viewport = Some(LogicalRect::new(pos(0.0, 0.0), size(800.0, 600.0)));
cache.reset_incremental();
assert!(cache.tree.is_none());
assert!(cache.cache_map.entries.is_empty());
assert!(cache.cached_display_list.is_none());
assert_eq!(cache.prev_dom_ptr, 0);
assert!(cache.counters.is_empty());
assert!(cache.float_cache.is_empty());
assert_eq!(cache.calculated_positions.len(), 1);
assert_eq!(cache.scroll_ids.len(), 1);
assert!(cache.viewport.is_some());
cache.reset_incremental();
assert!(cache.tree.is_none());
assert_eq!(
cache.memory_report().total_bytes(),
size_of::<LogicalPosition>() + size_of::<usize>() + size_of::<u64>()
);
}
#[test]
fn reconciliation_result_default_is_clean() {
let r = ReconciliationResult::default();
assert!(r.is_clean());
assert!(!r.needs_layout());
assert!(!r.needs_paint_only());
}
#[test]
fn reconciliation_result_predicates_hold_over_every_combination() {
for intrinsic in [false, true] {
for roots in [false, true] {
for paint in [false, true] {
let mut r = ReconciliationResult::default();
if intrinsic {
r.intrinsic_dirty.insert(0);
}
if roots {
r.layout_roots.insert(usize::MAX);
}
if paint {
r.paint_dirty.insert(7);
}
let expect_layout = intrinsic || roots;
assert_eq!(r.needs_layout(), expect_layout);
assert_eq!(r.is_clean(), !intrinsic && !roots && !paint);
assert_eq!(r.needs_paint_only(), !expect_layout && paint);
assert!(!(r.is_clean() && (r.needs_layout() || r.needs_paint_only())));
assert!(!(r.needs_layout() && r.needs_paint_only()));
}
}
}
}
#[test]
fn to_overflow_behavior_maps_every_layout_overflow_variant() {
assert_eq!(
to_overflow_behavior(MultiValue::Exact(LayoutOverflow::Visible)),
fc::OverflowBehavior::Visible
);
assert_eq!(
to_overflow_behavior(MultiValue::Exact(LayoutOverflow::Hidden)),
fc::OverflowBehavior::Hidden
);
assert_eq!(
to_overflow_behavior(MultiValue::Exact(LayoutOverflow::Scroll)),
fc::OverflowBehavior::Scroll
);
assert_eq!(
to_overflow_behavior(MultiValue::Exact(LayoutOverflow::Auto)),
fc::OverflowBehavior::Auto
);
assert_eq!(
to_overflow_behavior(MultiValue::Exact(LayoutOverflow::Clip)),
fc::OverflowBehavior::Hidden
);
}
#[test]
fn to_overflow_behavior_falls_back_to_the_initial_value() {
for mv in [MultiValue::Auto, MultiValue::Initial, MultiValue::Inherit] {
assert_eq!(to_overflow_behavior(mv), fc::OverflowBehavior::Visible);
}
}
#[test]
fn overflow_auto_keyword_is_a_typed_value_not_a_css_wide_keyword() {
let sd = styled(
Dom::create_body().with_child(div_class("s")),
".s { overflow-x: auto; overflow-y: scroll; }",
);
let id = NodeId::new(1);
let state = sd.styled_nodes.as_container()[id].styled_node_state;
assert_eq!(
to_overflow_behavior(get_overflow_x(&sd, id, &state)),
fc::OverflowBehavior::Auto
);
assert_eq!(
to_overflow_behavior(get_overflow_y(&sd, id, &state)),
fc::OverflowBehavior::Scroll
);
}
#[test]
fn style_text_align_to_fc_maps_every_variant() {
assert!(matches!(
style_text_align_to_fc(StyleTextAlign::Start),
fc::TextAlign::Start
));
assert!(matches!(
style_text_align_to_fc(StyleTextAlign::Left),
fc::TextAlign::Start
));
assert!(matches!(
style_text_align_to_fc(StyleTextAlign::End),
fc::TextAlign::End
));
assert!(matches!(
style_text_align_to_fc(StyleTextAlign::Right),
fc::TextAlign::End
));
assert!(matches!(
style_text_align_to_fc(StyleTextAlign::Center),
fc::TextAlign::Center
));
assert!(matches!(
style_text_align_to_fc(StyleTextAlign::Justify),
fc::TextAlign::Justify
));
}
#[test]
fn should_use_content_height_for_css_wide_keywords_and_auto() {
assert!(should_use_content_height(&MultiValue::Auto));
assert!(should_use_content_height(&MultiValue::Initial));
assert!(should_use_content_height(&MultiValue::Inherit));
assert!(should_use_content_height(&MultiValue::Exact(LayoutHeight::Auto)));
}
#[test]
fn should_use_content_height_is_false_for_definite_lengths() {
for pv in [
PixelValue::px(100.0),
PixelValue::px(-10.0),
PixelValue::percent(50.0),
PixelValue::percent(0.0),
PixelValue::em(2.0),
PixelValue::rem(2.0),
] {
assert!(
!should_use_content_height(&MultiValue::Exact(LayoutHeight::Px(pv))),
"expected a definite height for {pv:?}"
);
}
}
#[test]
fn should_use_content_height_treats_zero_px_as_content_based() {
assert!(should_use_content_height(&MultiValue::Exact(LayoutHeight::Px(
PixelValue::zero()
))));
assert!(should_use_content_height(&MultiValue::Exact(LayoutHeight::Px(
PixelValue::px(0.0)
))));
assert!(!should_use_content_height(&MultiValue::Exact(LayoutHeight::Px(
PixelValue::percent(0.0)
))));
}
#[test]
fn should_use_content_height_treats_non_px_metrics_as_content_based() {
for metric in [
SizeMetric::Pt,
SizeMetric::Vh,
SizeMetric::Vw,
SizeMetric::Cm,
SizeMetric::Mm,
SizeMetric::In,
] {
let pv = PixelValue::from_metric(metric, 100.0);
assert!(
should_use_content_height(&MultiValue::Exact(LayoutHeight::Px(pv))),
"{metric:?} is currently treated as content-based"
);
}
}
#[test]
fn should_use_content_height_for_intrinsic_keywords_but_not_calc() {
assert!(should_use_content_height(&MultiValue::Exact(LayoutHeight::MinContent)));
assert!(should_use_content_height(&MultiValue::Exact(LayoutHeight::MaxContent)));
assert!(should_use_content_height(&MultiValue::Exact(LayoutHeight::FitContent(
PixelValue::px(10.0)
))));
assert!(!should_use_content_height(&MultiValue::Exact(LayoutHeight::Calc(
CalcAstItemVec::from_vec(Vec::new())
))));
}
fn one_node_tree_with(bp: &ResolvedBoxProps) -> LayoutTree {
build_tree(
vec![hot(None, None, Some(size(100.0, 100.0)), bp)],
warm_default(1),
&[vec![]],
)
}
#[test]
fn apply_content_based_height_keeps_the_larger_of_min_height_and_content() {
let bp = box_props(
edges(0.0, 0.0, 0.0, 0.0),
edges(0.0, 0.0, 0.0, 0.0),
edges(10.0, 0.0, 10.0, 0.0), );
let tree = one_node_tree_with(&bp);
let wm = LayoutWritingMode::HorizontalTb;
let out = apply_content_based_height(size(100.0, 100.0), size(0.0, 50.0), &tree, 0, wm)
.expect("valid node");
assert_eq!(out, size(100.0, 100.0));
let out = apply_content_based_height(size(100.0, 40.0), size(0.0, 50.0), &tree, 0, wm)
.expect("valid node");
assert_eq!(out, size(100.0, 70.0));
}
#[test]
fn apply_content_based_height_at_zero_and_in_vertical_writing_modes() {
let tree = one_node_tree_with(&zero_box_props());
let out = apply_content_based_height(
size(0.0, 0.0),
size(0.0, 0.0),
&tree,
0,
LayoutWritingMode::HorizontalTb,
)
.expect("valid node");
assert_eq!(out, size(0.0, 0.0));
let out = apply_content_based_height(
size(10.0, 80.0),
size(50.0, 0.0),
&tree,
0,
LayoutWritingMode::VerticalRl,
)
.expect("valid node");
assert_eq!(out, size(50.0, 80.0));
}
#[test]
fn apply_content_based_height_does_not_propagate_nan() {
let tree = one_node_tree_with(&zero_box_props());
let wm = LayoutWritingMode::HorizontalTb;
let out = apply_content_based_height(size(100.0, 40.0), size(0.0, f32::NAN), &tree, 0, wm)
.expect("valid node");
assert!(!out.height.is_nan());
assert_eq!(out.height, 40.0);
let out =
apply_content_based_height(size(100.0, f32::NAN), size(0.0, 50.0), &tree, 0, wm)
.expect("valid node");
assert!(!out.height.is_nan());
assert_eq!(out.height, 50.0);
}
#[test]
fn apply_content_based_height_saturates_at_infinity_and_rejects_bad_indices() {
let tree = one_node_tree_with(&zero_box_props());
let wm = LayoutWritingMode::HorizontalTb;
let out =
apply_content_based_height(size(10.0, 10.0), size(0.0, f32::INFINITY), &tree, 0, wm)
.expect("valid node");
assert!(out.height.is_infinite() && out.height.is_sign_positive());
let out = apply_content_based_height(size(10.0, 10.0), size(0.0, f32::MAX), &tree, 0, wm)
.expect("valid node");
assert_eq!(out.height, f32::MAX);
assert!(apply_content_based_height(size(1.0, 1.0), size(1.0, 1.0), &tree, 1, wm).is_err());
assert!(
apply_content_based_height(size(1.0, 1.0), size(1.0, 1.0), &tree, usize::MAX, wm)
.is_err()
);
}
#[test]
fn subtree_hash_is_deterministic() {
let a = calculate_subtree_hash(42, &[1, 2, 3]);
let b = calculate_subtree_hash(42, &[1, 2, 3]);
assert_eq!(a, b);
assert_eq!(a, calculate_subtree_hash(42, &[1, 2, 3]));
}
#[test]
fn subtree_hash_depends_on_child_order_and_arity() {
let base = calculate_subtree_hash(1, &[10, 20]);
assert_ne!(base, calculate_subtree_hash(1, &[20, 10]), "order must matter");
assert_ne!(base, calculate_subtree_hash(1, &[10, 20, 30]), "arity must matter");
assert_ne!(base, calculate_subtree_hash(1, &[10]), "a dropped child must matter");
assert_ne!(base, calculate_subtree_hash(2, &[10, 20]), "self hash must matter");
}
#[test]
fn subtree_hash_distinguishes_no_children_from_a_zero_child() {
assert_ne!(
calculate_subtree_hash(0, &[]),
calculate_subtree_hash(0, &[0])
);
assert_ne!(
calculate_subtree_hash(0, &[0]),
calculate_subtree_hash(0, &[0, 0])
);
}
#[test]
fn subtree_hash_does_not_swap_self_hash_and_child_hash() {
assert_ne!(
calculate_subtree_hash(7, &[9]),
calculate_subtree_hash(9, &[7])
);
}
#[test]
fn subtree_hash_handles_extremes_and_large_child_lists() {
let extremes = calculate_subtree_hash(u64::MAX, &[u64::MAX, 0, u64::MAX]);
assert_eq!(
extremes,
calculate_subtree_hash(u64::MAX, &[u64::MAX, 0, u64::MAX])
);
assert_ne!(extremes, calculate_subtree_hash(0, &[0, 0, 0]));
let many: Vec<u64> = (0..10_000).collect();
assert_eq!(
calculate_subtree_hash(1, &many),
calculate_subtree_hash(1, &many)
);
}
#[test]
fn content_box_pos_adds_border_and_padding_but_not_margin() {
let bp = box_props(
edges(999.0, 999.0, 999.0, 999.0), edges(2.0, 0.0, 0.0, 3.0), edges(5.0, 0.0, 0.0, 7.0), );
let out = calculate_content_box_pos(pos(10.0, 20.0), &bp);
assert_eq!(out, pos(10.0 + 3.0 + 7.0, 20.0 + 2.0 + 5.0));
assert_eq!(
calculate_content_box_pos(pos(4.0, 5.0), &zero_box_props()),
pos(4.0, 5.0)
);
}
#[test]
fn content_box_pos_with_negative_edges_shifts_backwards() {
let bp = box_props(
edges(0.0, 0.0, 0.0, 0.0),
edges(-1.0, 0.0, 0.0, -2.0),
edges(-3.0, 0.0, 0.0, -4.0),
);
assert_eq!(
calculate_content_box_pos(pos(0.0, 0.0), &bp),
pos(-6.0, -4.0)
);
}
#[test]
fn content_box_pos_with_non_finite_edges_is_deterministic() {
let nan_bp = box_props(
edges(0.0, 0.0, 0.0, 0.0),
edges(f32::NAN, 0.0, 0.0, f32::NAN),
edges(0.0, 0.0, 0.0, 0.0),
);
let out = calculate_content_box_pos(pos(1.0, 1.0), &nan_bp);
assert!(out.x.is_nan() && out.y.is_nan(), "NaN edges propagate, no panic");
let inf_bp = box_props(
edges(0.0, 0.0, 0.0, 0.0),
edges(f32::INFINITY, 0.0, 0.0, f32::INFINITY),
edges(0.0, 0.0, 0.0, 0.0),
);
let out = calculate_content_box_pos(pos(f32::NEG_INFINITY, f32::NEG_INFINITY), &inf_bp);
assert!(out.x.is_nan() && out.y.is_nan());
let max_bp = box_props(
edges(0.0, 0.0, 0.0, 0.0),
edges(f32::MAX, 0.0, 0.0, f32::MAX),
edges(f32::MAX, 0.0, 0.0, f32::MAX),
);
let out = calculate_content_box_pos(pos(f32::MAX, f32::MAX), &max_bp);
assert!(out.x.is_infinite() && out.x.is_sign_positive());
assert!(out.y.is_infinite() && out.y.is_sign_positive());
}
fn scrollbars(h: bool, v: bool, w: f32) -> ScrollbarRequirements {
ScrollbarRequirements {
needs_horizontal: h,
needs_vertical: v,
scrollbar_width: w,
scrollbar_height: w,
visual_width_px: w,
}
}
fn tree_with_scrollbar_info(info: Option<ScrollbarRequirements>) -> LayoutTree {
let mut warm = warm_default(1);
warm[0].scrollbar_info = info;
build_tree(vec![plain(None)], warm, &[vec![]])
}
#[test]
fn check_scrollbar_change_skip_flag_short_circuits_everything() {
let tree = tree_with_scrollbar_info(Some(scrollbars(false, false, 0.0)));
assert!(!check_scrollbar_change(&tree, 0, &scrollbars(true, true, 16.0), true));
}
#[test]
fn check_scrollbar_change_out_of_range_node_is_false() {
let tree = tree_with_scrollbar_info(None);
assert!(!check_scrollbar_change(&tree, 1, &scrollbars(true, true, 16.0), false));
assert!(!check_scrollbar_change(&tree, usize::MAX, &scrollbars(true, true, 16.0), false));
}
#[test]
fn check_scrollbar_change_without_previous_info_uses_needs_reflow() {
let tree = tree_with_scrollbar_info(None);
assert!(!check_scrollbar_change(&tree, 0, &scrollbars(true, true, 0.0), false));
assert!(check_scrollbar_change(&tree, 0, &scrollbars(false, false, 16.0), false));
assert!(!check_scrollbar_change(&tree, 0, &scrollbars(true, true, f32::NAN), false));
}
#[test]
fn check_scrollbar_change_detects_both_addition_and_removal() {
let had_vertical = tree_with_scrollbar_info(Some(scrollbars(false, true, 16.0)));
assert!(check_scrollbar_change(&had_vertical, 0, &scrollbars(false, false, 0.0), false));
assert!(check_scrollbar_change(&had_vertical, 0, &scrollbars(true, true, 16.0), false));
assert!(!check_scrollbar_change(&had_vertical, 0, &scrollbars(false, true, 16.0), false));
}
#[test]
fn check_scrollbar_change_ignores_width_only_changes() {
let tree = tree_with_scrollbar_info(Some(scrollbars(false, true, 16.0)));
assert!(!check_scrollbar_change(&tree, 0, &scrollbars(false, true, 4.0), false));
}
fn shift_fixture() -> (LayoutTree, PositionVec) {
let tree = build_tree(
vec![plain(None), plain(Some(0)), plain(Some(0)), plain(Some(1))],
warm_default(4),
&[vec![1, 2], vec![3], vec![], vec![]],
);
let positions = vec![
pos(0.0, 0.0),
pos(10.0, 10.0),
pos(20.0, 20.0),
pos(30.0, 30.0),
];
(tree, positions)
}
#[test]
fn shift_subtree_position_zero_delta_is_identity() {
let (tree, mut positions) = shift_fixture();
let before = positions.clone();
shift_subtree_position(0, pos(0.0, 0.0), &tree, &mut positions);
assert_eq!(positions, before);
}
#[test]
fn shift_subtree_position_moves_the_subtree_and_leaves_siblings_alone() {
let (tree, mut positions) = shift_fixture();
shift_subtree_position(1, pos(5.0, -3.0), &tree, &mut positions);
assert_eq!(positions[0], pos(0.0, 0.0), "parent untouched");
assert_eq!(positions[1], pos(15.0, 7.0), "shifted node");
assert_eq!(positions[2], pos(20.0, 20.0), "sibling untouched");
assert_eq!(positions[3], pos(35.0, 27.0), "descendant follows");
}
#[test]
fn shift_subtree_position_out_of_range_node_is_a_noop() {
let (tree, mut positions) = shift_fixture();
let before = positions.clone();
shift_subtree_position(99, pos(5.0, 5.0), &tree, &mut positions);
shift_subtree_position(usize::MAX, pos(5.0, 5.0), &tree, &mut positions);
assert_eq!(positions, before);
}
#[test]
fn shift_subtree_position_skips_nodes_without_a_stored_position() {
let (tree, _) = shift_fixture();
let mut positions = vec![pos(1.0, 1.0)];
shift_subtree_position(0, pos(2.0, 2.0), &tree, &mut positions);
assert_eq!(positions.len(), 1, "the vec is not grown by the shift");
assert_eq!(positions[0], pos(3.0, 3.0));
}
#[test]
fn shift_subtree_position_leaves_the_unset_sentinel_unset() {
let (tree, _) = shift_fixture();
let mut positions = vec![pos(0.0, 0.0), POSITION_UNSET, POSITION_UNSET, POSITION_UNSET];
shift_subtree_position(0, pos(7.0, 9.0), &tree, &mut positions);
assert_eq!(pos_get(&positions, 0), Some(pos(7.0, 9.0)));
assert!(pos_get(&positions, 1).is_none());
assert!(pos_get(&positions, 3).is_none());
}
#[test]
fn shift_subtree_position_nan_delta_is_confined_to_the_subtree() {
let (tree, mut positions) = shift_fixture();
shift_subtree_position(1, pos(f32::NAN, f32::NAN), &tree, &mut positions);
assert!(positions[1].x.is_nan() && positions[1].y.is_nan());
assert!(positions[3].x.is_nan(), "NaN reaches the descendant");
assert!(!positions[2].x.is_nan(), "the sibling is untouched");
assert_eq!(positions[0], pos(0.0, 0.0));
}
#[test]
fn shift_subtree_position_walks_a_deep_chain_without_blowing_the_stack() {
const DEPTH: usize = 500;
let mut nodes = vec![plain(None)];
let mut child_lists: Vec<Vec<usize>> = vec![vec![1]];
for i in 1..DEPTH {
nodes.push(plain(Some(i - 1)));
child_lists.push(if i + 1 < DEPTH { vec![i + 1] } else { vec![] });
}
let tree = build_tree(nodes, warm_default(DEPTH), &child_lists);
let mut positions = vec![pos(0.0, 0.0); DEPTH];
shift_subtree_position(0, pos(1.0, 2.0), &tree, &mut positions);
assert_eq!(positions[0], pos(1.0, 2.0));
assert_eq!(positions[DEPTH - 1], pos(1.0, 2.0));
}
#[test]
fn position_bfc_child_descendants_out_of_range_node_is_a_noop() {
let (tree, mut positions) = shift_fixture();
let before = positions.clone();
position_bfc_child_descendants(&tree, 99, pos(1.0, 1.0), &mut positions);
assert_eq!(positions, before);
}
#[test]
fn position_bfc_child_descendants_converts_relative_to_absolute() {
let bp = box_props(
edges(0.0, 0.0, 0.0, 0.0),
edges(2.0, 0.0, 0.0, 2.0),
edges(3.0, 0.0, 0.0, 3.0),
);
let mut warm = warm_default(3);
warm[1].relative_position = Some(pos(10.0, 20.0));
warm[2].relative_position = Some(pos(1.0, 1.0));
let tree = build_tree(
vec![
plain(None),
hot(Some(0), None, Some(size(50.0, 50.0)), &bp),
plain(Some(1)),
],
warm,
&[vec![1], vec![2], vec![]],
);
let mut positions: PositionVec = Vec::new();
position_bfc_child_descendants(&tree, 0, pos(100.0, 200.0), &mut positions);
assert_eq!(pos_get(&positions, 1), Some(pos(110.0, 220.0)));
assert_eq!(pos_get(&positions, 2), Some(pos(110.0 + 5.0 + 1.0, 220.0 + 5.0 + 1.0)));
}
#[test]
fn position_bfc_child_descendants_defaults_a_missing_relative_position_to_the_origin() {
let tree = build_tree(
vec![plain(None), plain(Some(0))],
warm_default(2), &[vec![1], vec![]],
);
let mut positions: PositionVec = Vec::new();
position_bfc_child_descendants(&tree, 0, pos(7.0, 8.0), &mut positions);
assert_eq!(pos_get(&positions, 1), Some(pos(7.0, 8.0)));
}
#[test]
fn position_flex_child_descendants_rejects_a_dangling_child_index() {
let mut tree = build_tree(vec![plain(None)], warm_default(1), &[vec![99]]);
let mut positions: PositionVec = Vec::new();
let r = position_flex_child_descendants(
&mut tree,
0,
pos(0.0, 0.0),
size(100.0, 100.0),
&mut positions,
);
assert!(r.is_err());
}
#[test]
fn position_flex_child_descendants_positions_children_and_grandchildren() {
let mut warm = warm_default(3);
warm[1].relative_position = Some(pos(5.0, 5.0));
warm[2].relative_position = Some(pos(2.0, 2.0));
let mut tree = build_tree(
vec![plain(None), plain(Some(0)), plain(Some(1))],
warm,
&[vec![1], vec![2], vec![]],
);
let mut positions: PositionVec = Vec::new();
position_flex_child_descendants(
&mut tree,
0,
pos(50.0, 60.0),
size(100.0, 100.0),
&mut positions,
)
.expect("well-formed tree");
assert_eq!(pos_get(&positions, 1), Some(pos(55.0, 65.0)));
assert_eq!(pos_get(&positions, 2), Some(pos(57.0, 67.0)));
}
#[test]
fn collect_children_dom_ids_skips_display_none_children() {
let sd = styled(
Dom::create_body()
.with_child(Dom::create_div())
.with_child(div_class("hide"))
.with_child(Dom::create_div()),
".hide { display: none; }",
);
let children = collect_children_dom_ids(&sd, NodeId::ZERO);
assert_eq!(children, vec![NodeId::new(1), NodeId::new(3)]);
}
#[test]
fn collect_children_dom_ids_for_leaves_and_unknown_parents_is_empty() {
let sd = styled(Dom::create_body().with_child(Dom::create_div()), "");
assert!(collect_children_dom_ids(&sd, NodeId::new(1)).is_empty());
assert!(collect_children_dom_ids(&sd, NodeId::new(999)).is_empty());
assert!(collect_children_dom_ids(&sd, NodeId::new(usize::MAX)).is_empty());
}
#[test]
fn layout_relevant_child_count_counts_only_boxes_the_builder_would_emit() {
let sd = styled(
Dom::create_body()
.with_child(Dom::create_div())
.with_child(Dom::create_text(" "))
.with_child(div_class("hide")),
".hide { display: none; }",
);
let children = [NodeId::new(1), NodeId::new(2), NodeId::new(3)];
assert_eq!(layout_relevant_child_count(&sd, &children, NodeId::ZERO), 1);
assert_eq!(layout_relevant_child_count(&sd, &[], NodeId::ZERO), 0);
}
#[test]
fn is_whitespace_only_inline_run_empty_run_is_true() {
let sd = whitespace_dom("");
assert!(is_whitespace_only_inline_run(&sd, &[], NodeId::new(1)));
}
#[test]
fn is_whitespace_only_inline_run_detects_collapsible_whitespace_text() {
let sd = whitespace_dom("");
assert!(is_whitespace_only_inline_run(&sd, &[(0, NodeId::new(2))], NodeId::new(1)));
assert!(!is_whitespace_only_inline_run(&sd, &[(1, NodeId::new(3))], NodeId::new(1)));
assert!(!is_whitespace_only_inline_run(
&sd,
&[(0, NodeId::new(2)), (1, NodeId::new(3))],
NodeId::new(1)
));
}
#[test]
fn is_whitespace_only_inline_run_rejects_elements_and_non_ascii_spaces() {
let sd = whitespace_dom("");
assert!(!is_whitespace_only_inline_run(&sd, &[(2, NodeId::new(4))], NodeId::new(1)));
assert!(!is_whitespace_only_inline_run(&sd, &[(3, NodeId::new(5))], NodeId::new(1)));
}
#[test]
fn is_whitespace_only_inline_run_respects_white_space_pre() {
let sd = whitespace_dom(".p { white-space: pre; }");
assert!(!is_whitespace_only_inline_run(&sd, &[(0, NodeId::new(2))], NodeId::new(1)));
let sd = whitespace_dom(".p { white-space: normal; }");
assert!(is_whitespace_only_inline_run(&sd, &[(0, NodeId::new(2))], NodeId::new(1)));
}
#[test]
fn reposition_clean_subtrees_ignores_empty_and_dangling_roots() {
let sd = styled(Dom::create_body().with_child(Dom::create_div()), "");
let tree = build_tree(
vec![
hot(None, Some(NodeId::ZERO), Some(size(100.0, 100.0)), &zero_box_props()),
plain(Some(0)),
],
warm_default(2),
&[vec![1], vec![]],
);
let mut positions = vec![pos(0.0, 0.0), pos(1.0, 1.0)];
let before = positions.clone();
reposition_clean_subtrees(&sd, &tree, &BTreeSet::new(), &mut positions);
assert_eq!(positions, before);
let mut roots = BTreeSet::new();
roots.insert(99);
roots.insert(usize::MAX);
reposition_clean_subtrees(&sd, &tree, &roots, &mut positions);
assert_eq!(positions, before);
}
#[test]
fn reposition_clean_subtrees_skips_flex_parents() {
let sd = styled(Dom::create_body().with_child(Dom::create_div()), "");
let mut nodes = vec![
hot(None, Some(NodeId::ZERO), Some(size(100.0, 100.0)), &zero_box_props()),
plain(Some(0)),
];
nodes[0].formatting_context = FormattingContext::Flex;
let tree = build_tree(nodes, warm_default(2), &[vec![1], vec![]]);
let mut positions = vec![pos(0.0, 0.0), pos(1.0, 1.0)];
let before = positions.clone();
let mut roots = BTreeSet::new();
roots.insert(1);
reposition_clean_subtrees(&sd, &tree, &roots, &mut positions);
assert_eq!(positions, before);
}
#[test]
fn reposition_block_flow_siblings_stacks_clean_children_from_the_content_origin() {
let sd = styled(Dom::create_body().with_child(Dom::create_div()), "");
let parent_bp = box_props(
edges(0.0, 0.0, 0.0, 0.0),
edges(5.0, 5.0, 5.0, 5.0), edges(3.0, 3.0, 3.0, 3.0), );
let tree = build_tree(
vec![
hot(None, Some(NodeId::ZERO), Some(size(200.0, 200.0)), &parent_bp),
hot(Some(0), None, Some(size(200.0, 50.0)), &zero_box_props()),
hot(Some(0), None, Some(size(200.0, 30.0)), &zero_box_props()),
plain(Some(1)),
],
warm_default(4),
&[vec![1, 2], vec![3], vec![], vec![]],
);
let mut positions = vec![
pos(10.0, 20.0), pos(0.0, 0.0),
pos(0.0, 0.0),
pos(0.0, 0.0),
];
reposition_block_flow_siblings(&sd, 0, &tree, &BTreeSet::new(), &mut positions);
assert_eq!(positions[1], pos(13.0, 23.0));
assert_eq!(positions[2], pos(13.0, 73.0), "second child stacked below the first");
assert_eq!(positions[3], pos(13.0, 23.0));
}
#[test]
fn compute_counters_on_an_empty_or_anonymous_tree_does_not_panic() {
let sd = styled(Dom::create_body(), "");
let mut counters: HashMap<(usize, String), i32> = HashMap::new();
let empty = build_tree(Vec::new(), Vec::new(), &[]);
compute_counters(&sd, &empty, &mut counters);
assert!(counters.is_empty());
let anon = build_tree(
vec![plain(None), plain(Some(0))],
warm_default(2),
&[vec![1], vec![]],
);
compute_counters(&sd, &anon, &mut counters);
assert!(counters.is_empty());
}
#[test]
fn compute_counters_increments_the_list_item_counter_in_document_order() {
let sd = styled(
Dom::create_body()
.with_child(div_class("li"))
.with_child(div_class("li")),
".li { display: list-item; }",
);
let tree = build_tree(
vec![
hot(None, Some(NodeId::ZERO), Some(size(100.0, 100.0)), &zero_box_props()),
hot(Some(0), Some(NodeId::new(1)), Some(size(100.0, 10.0)), &zero_box_props()),
hot(Some(0), Some(NodeId::new(2)), Some(size(100.0, 10.0)), &zero_box_props()),
],
warm_default(3),
&[vec![1, 2], vec![], vec![]],
);
let mut counters: HashMap<(usize, String), i32> = HashMap::new();
compute_counters(&sd, &tree, &mut counters);
assert_eq!(counters.get(&(1, "list-item".to_string())), Some(&1));
assert_eq!(counters.get(&(2, "list-item".to_string())), Some(&2));
assert_eq!(counters.get(&(0, "list-item".to_string())), None);
}
#[test]
fn compute_counters_leaves_plain_elements_alone() {
let sd = styled(
Dom::create_body().with_child(Dom::create_div()),
"",
);
let tree = build_tree(
vec![
hot(None, Some(NodeId::ZERO), Some(size(100.0, 100.0)), &zero_box_props()),
hot(Some(0), Some(NodeId::new(1)), Some(size(100.0, 10.0)), &zero_box_props()),
],
warm_default(2),
&[vec![1], vec![]],
);
let mut counters: HashMap<(usize, String), i32> = HashMap::new();
compute_counters(&sd, &tree, &mut counters);
assert!(counters.is_empty(), "no counter-reset/increment → no counters");
}
}