use std::{
collections::{BTreeMap, BTreeSet},
hash::{DefaultHasher, Hash, Hasher},
};
use azul_core::{
dom::{FormattingContext, NodeId, NodeType},
geom::{LogicalPosition, LogicalRect, LogicalSize},
styled_dom::{StyledDom, StyledNode},
};
use azul_css::{
css::CssPropertyValue,
props::{
layout::{
LayoutDisplay, LayoutFlexWrap, LayoutHeight, LayoutJustifyContent, LayoutOverflow,
LayoutPosition, LayoutWrap, 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_justify_content, get_overflow_x,
get_overflow_y, get_text_align, get_white_space_property, get_wrap, get_writing_mode,
MultiValue,
},
layout_tree::{
is_block_level, AnonymousBoxType, LayoutNode, 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(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;
}
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 = if width_type == AvailableWidthType::MinContent { 1 } else { 0 };
let h = if height_type == AvailableWidthType::MinContent { 1 } else { 0 };
5 + w * 2 + h
}
}
}
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() < 0.1
&& (known_dims.height - entry.available_size.height).abs() < 0.1
{
return Some(entry);
}
if (known_dims.width - entry.result_size.width).abs() < 0.1
&& (known_dims.height - entry.result_size.height).abs() < 0.1
{
return Some(entry);
}
None
}
pub fn store_size(&mut self, slot: usize, entry: SizingCacheEntry) {
self.measure_entries[slot] = Some(entry);
self.is_empty = false;
}
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() < 0.1
&& (known_dims.height - entry.available_size.height).abs() < 0.1
{
return Some(entry);
}
if (known_dims.width - entry.result_size.width).abs() < 0.1
&& (known_dims.height - entry.result_size.height).abs() < 0.1
{
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]
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: &[LayoutNode]) {
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: BTreeMap<usize, u64>,
pub scroll_id_to_node_id: BTreeMap<u64, NodeId>,
pub counters: BTreeMap<(usize, String), i32>,
pub float_cache: BTreeMap<usize, fc::FloatingContext>,
pub cache_map: LayoutCacheMap,
}
#[derive(Debug, Default)]
pub struct ReconciliationResult {
pub intrinsic_dirty: BTreeSet<usize>,
pub layout_roots: BTreeSet<usize>,
}
impl ReconciliationResult {
pub fn is_clean(&self) -> bool {
self.intrinsic_dirty.is_empty() && self.layout_roots.is_empty()
}
}
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 parent_node = match tree.get(parent_idx) {
Some(n) => n,
None => continue,
};
match parent_node.formatting_context {
FormattingContext::Block { .. } | FormattingContext::TableRowGroup => {
reposition_block_flow_siblings(
styled_dom,
parent_idx,
parent_node,
tree,
layout_roots,
calculated_positions,
);
}
FormattingContext::Flex | FormattingContext::Grid => {
}
FormattingContext::Table | FormattingContext::TableRow => {
}
_ => { }
}
}
}
pub 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,
}
}
pub 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,
}
}
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 {
return children;
};
children.push(child_id);
while let Some(hierarchy_item) = hierarchy_container.get(child_id) {
let Some(next) = hierarchy_item.next_sibling_id() else {
break;
};
children.push(next);
child_id = next;
}
children
}
pub fn is_simple_flex_stack(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> bool {
let Some(id) = dom_id else { return false };
let binding = styled_dom.styled_nodes.as_container();
let styled_node = match binding.get(id) {
Some(styled_node) => styled_node,
None => return false,
};
let wrap = get_wrap(styled_dom, id, &styled_node.styled_node_state);
if wrap.unwrap_or_default() != LayoutFlexWrap::NoWrap {
return false;
}
let justify = get_justify_content(styled_dom, id, &styled_node.styled_node_state);
if !matches!(
justify.unwrap_or_default(),
LayoutJustifyContent::FlexStart | LayoutJustifyContent::Start
) {
return false;
}
true
}
pub fn reposition_block_flow_siblings(
styled_dom: &StyledDom,
parent_idx: usize,
parent_node: &LayoutNode,
tree: &LayoutTree,
layout_roots: &BTreeSet<usize>,
calculated_positions: &mut super::PositionVec,
) {
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.clone())
.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 content_box_origin = LogicalPosition::new(
parent_pos.x + parent_node.box_props.padding.left,
parent_pos.y + parent_node.box_props.padding.top,
);
let mut main_pen = 0.0;
for &child_idx in &parent_node.children {
let child_node = match tree.get(child_idx) {
Some(n) => n,
None => continue,
};
let child_size = child_node.used_size.unwrap_or_default();
let child_main_sum = child_node.box_props.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_node.box_props.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_node.box_props.margin.main_start(writing_mode);
let new_main_pos = main_pen + child_main_start;
let old_relative_pos = child_node.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;
}
}
}
pub 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) {
for &child_idx in &node.children {
shift_subtree_position(child_idx, delta, tree, calculated_positions);
}
}
}
pub fn reconcile_and_invalidate<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
cache: &LayoutCache,
viewport: LogicalRect,
) -> Result<(LayoutTree, ReconciliationResult)> {
let mut new_tree_builder = LayoutTreeBuilder::new(ctx.viewport_size);
let mut recon_result = ReconciliationResult::default();
let old_tree = cache.tree.as_ref();
if cache.viewport.map_or(true, |v| v.size != viewport.size) {
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,
&mut 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);
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) | Some(StyleWhiteSpace::PreWrap) | Some(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| c.is_whitespace()) {
return false; }
}
_ => {
return false; }
}
}
}
true }
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> {
let node_data = &styled_dom.node_data.as_container()[new_dom_id];
let old_node = old_tree.and_then(|t| old_tree_idx.and_then(|idx| t.get(idx)));
let new_node_data_hash = hash_styled_node_data(styled_dom, new_dom_id);
let is_dirty = old_node.map_or(true, |n| new_node_data_hash != n.node_data_hash);
let new_node_idx = if is_dirty {
new_tree_builder.create_node_from_dom(
styled_dom,
new_dom_id,
new_parent_idx,
debug_messages,
)?
} else {
new_tree_builder.clone_node_from_old(old_node.unwrap(), new_parent_idx)
};
{
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 new_children_dom_ids: Vec<_> = collect_children_dom_ids(styled_dom, new_dom_id);
let old_children_indices: Vec<_> = old_node.map(|n| n.children.clone()).unwrap_or_default();
let mut children_are_different = new_children_dom_ids.len() != old_children_indices.len();
let mut new_child_hashes = Vec::new();
let has_block_child = new_children_dom_ids
.iter()
.any(|&id| is_block_level(styled_dom, id));
if !has_block_child {
for (i, &new_child_dom_id) in new_children_dom_ids.iter().enumerate() {
let old_child_idx = 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.get(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<_>>()
)));
}
for (pos, inline_dom_id) in inline_run.drain(..) {
let old_child_idx = old_children_indices.get(pos).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);
}
}
recon.intrinsic_dirty.insert(anon_idx);
children_are_different = true;
} }
let old_child_idx = 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.get(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<_>>()
)));
}
for (pos, inline_dom_id) in inline_run.drain(..) {
let old_child_idx = old_children_indices.get(pos).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);
}
}
recon.intrinsic_dirty.insert(anon_idx);
children_are_different = true;
} }
}
let final_subtree_hash = calculate_subtree_hash(new_node_data_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 is_dirty || children_are_different {
recon.intrinsic_dirty.insert(new_node_idx);
recon.layout_roots.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>,
node: &LayoutNode,
containing_block_size: LogicalSize,
) -> Result<PreparedLayoutContext<'a>> {
let dom_id = node.dom_node_id;
let intrinsic = node.intrinsic_sizes.clone().unwrap_or_default();
let final_used_size = calculate_used_size_for_node(
ctx.styled_dom,
dom_id, containing_block_size,
intrinsic,
&node.box_props,
ctx.viewport_size,
)?;
let writing_mode = node.computed_style.writing_mode;
let text_align = node.computed_style.text_align;
let display = node.computed_style.display;
let overflow_y = node.computed_style.overflow_y;
let height_is_auto = node.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 scrollbar_reservation = match overflow_y {
LayoutOverflow::Scroll | LayoutOverflow::Auto => {
dom_id.map(|id| {
let styled_node_state = ctx.styled_dom.styled_nodes.as_container()
.get(id).map(|s| s.styled_node_state.clone()).unwrap_or_default();
crate::solver3::getters::get_layout_scrollbar_width_px(ctx, id, &styled_node_state)
}).unwrap_or(fc::DEFAULT_SCROLLBAR_WIDTH_PX)
}
_ => 0.0,
};
let available_size_for_children = if scrollbar_reservation > 0.0 {
LogicalSize {
width: (available_size_for_children.width - scrollbar_reservation).max(0.0),
height: available_size_for_children.height,
}
} else {
available_size_for_children
};
let constraints = LayoutConstraints {
available_size: available_size_for_children,
bfc_state: None,
writing_mode,
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.clone(),
})
}
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 {
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 container_size = box_props.inner_size(final_used_size, writing_mode);
let scrollbar_width_px =
crate::solver3::getters::get_layout_scrollbar_width_px(ctx, dom_id, styled_node_state);
fc::check_scrollbar_necessity(
content_size,
container_size,
to_overflow_behavior(overflow_x),
to_overflow_behavior(overflow_y),
scrollbar_width_px,
)
}
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(current_node) = tree.get(node_index) else {
return false;
};
match ¤t_node.scrollbar_info {
None => scrollbar_info.needs_reflow(),
Some(old_info) => {
let adding_horizontal = !old_info.needs_horizontal && scrollbar_info.needs_horizontal;
let adding_vertical = !old_info.needs_vertical && scrollbar_info.needs_vertical;
adding_horizontal || adding_vertical
}
}
}
fn merge_scrollbar_info(
tree: &LayoutTree,
node_index: usize,
new_info: &ScrollbarRequirements,
) -> ScrollbarRequirements {
let Some(current_node) = tree.get(node_index) else {
return new_info.clone();
};
match ¤t_node.scrollbar_info {
Some(old) => {
let effective_width = if new_info.scrollbar_width > 0.0 {
new_info.scrollbar_width
} else {
old.scrollbar_width
};
let effective_height = if new_info.scrollbar_height > 0.0 {
new_info.scrollbar_height
} else {
old.scrollbar_height
};
ScrollbarRequirements {
needs_horizontal: old.needs_horizontal || new_info.needs_horizontal,
needs_vertical: old.needs_vertical || new_info.needs_vertical,
scrollbar_width: if old.needs_vertical || new_info.needs_vertical {
effective_width
} else {
0.0
},
scrollbar_height: if old.needs_horizontal || new_info.needs_horizontal {
effective_height
} else {
0.0
},
}
}
None => new_info.clone(),
}
}
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: &LayoutNode,
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(|n| format!("{:?}", n.node_type))
.unwrap_or_else(|| "Unknown".to_string());
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,
current_node.box_props.border.left,
current_node.box_props.border.top,
current_node.box_props.padding.left,
current_node.box_props.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: &LayoutNode,
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(|n| format!("{:?}", n.node_type))
.unwrap_or_else(|| "Unknown".to_string());
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.margin.left,
child_node.box_props.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: &mut 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: &mut bool,
float_cache: &mut BTreeMap<usize, fc::FloatingContext>,
) -> Result<()> {
let child_node = tree.get_mut(child_index).ok_or(LayoutError::InvalidTree)?;
child_node.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_content_box_pos =
calculate_content_box_pos(child_absolute_pos, &child_node.box_props);
let child_inner_size = child_node
.box_props
.inner_size(child_node.used_size.unwrap_or_default(), writing_mode);
let child_children: Vec<usize> = child_node.children.clone();
let child_fc = child_node.formatting_context.clone();
if !child_children.is_empty() {
if is_flex_or_grid {
position_flex_child_descendants(
ctx,
tree,
text_cache,
child_index,
child_content_box_pos,
child_inner_size,
calculated_positions,
reflow_needed_for_scrollbars,
float_cache,
)?;
} 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 &node.children {
let Some(child_node) = tree.get(child_index) else { continue };
let child_rel_pos = child_node.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 child_content_box_pos = LogicalPosition::new(
child_abs_pos.x + child_node.box_props.border.left + child_node.box_props.padding.left,
child_abs_pos.y + child_node.box_props.border.top + child_node.box_props.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,
calculated_positions: &mut super::PositionVec,
) -> Result<()> {
let out_of_flow_children: Vec<(usize, Option<NodeId>)> = {
let current_node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
current_node
.children
.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);
set_static_positions_recursive(
ctx,
tree,
text_cache,
child_index,
self_content_box_pos,
calculated_positions,
)?;
}
Ok(())
}
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 BTreeMap<usize, fc::FloatingContext>,
compute_mode: ComputeMode,
) -> Result<()> {
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)
.cloned();
if let Some(cached_sizing) = sizing_hit {
if let Some(node) = tree.get_mut(node_index) {
node.used_size = Some(cached_sizing.result_size);
node.escaped_top_margin = cached_sizing.escaped_top_margin;
node.escaped_bottom_margin = cached_sizing.escaped_bottom_margin;
node.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 {
if let Some(node) = tree.get_mut(node_index) {
node.used_size = Some(cached_layout.result_size);
node.overflow_content_size = Some(cached_layout.content_size);
node.scrollbar_info = Some(cached_layout.scrollbar_info.clone());
}
return Ok(());
}
}
ComputeMode::PerformLayout => {
let layout_hit = ctx.cache_map.entries[node_index]
.get_layout(containing_block_size)
.cloned();
if let Some(cached_layout) = layout_hit {
if let Some(node) = tree.get_mut(node_index) {
node.used_size = Some(cached_layout.result_size);
node.overflow_content_size = Some(cached_layout.content_size);
node.scrollbar_info = Some(cached_layout.scrollbar_info.clone());
}
let box_props = tree.get(node_index)
.map(|n| n.box_props.clone())
.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 child_available_size = box_props.inner_size(
result_size,
LayoutWritingMode::HorizontalTb,
);
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(());
}
}
}
}
let PreparedLayoutContext {
constraints,
dom_id,
writing_mode,
mut final_used_size,
box_props,
} = {
let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
prepare_layout_context(ctx, node, containing_block_size)?
};
let layout_result =
layout_formatting_context(ctx, tree, text_cache, node_index, &constraints, float_cache)?;
let content_size = layout_result.output.overflow_size;
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.map_or(false, |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) | MultiValue::Exact(LayoutOverflow::Auto))
|| matches!(ov_y, MultiValue::Exact(LayoutOverflow::Scroll) | MultiValue::Exact(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 = match dom_id {
Some(id) => compute_scrollbar_info(
ctx,
id,
&styled_node_state,
content_size,
&box_props,
final_used_size,
writing_mode,
),
None => ScrollbarRequirements::default(),
};
if check_scrollbar_change(tree, node_index, &scrollbar_info, skip_scrollbar_check) {
*reflow_needed_for_scrollbars = true;
}
let merged_scrollbar_info = merge_scrollbar_info(tree, node_index, &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);
}
current_node.scrollbar_info = Some(merged_scrollbar_info.clone());
current_node.overflow_content_size = Some(content_size);
let pos = calculate_content_box_pos(containing_block_pos, ¤t_node.box_props);
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,
calculated_positions,
)?;
if node_index < ctx.cache_map.entries.len() {
let node_ref = tree.get(node_index);
let baseline = node_ref.and_then(|n| n.baseline);
let escaped_top = node_ref.and_then(|n| n.escaped_top_margin);
let escaped_bottom = node_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.clone(),
escaped_top_margin: escaped_top,
escaped_bottom_margin: escaped_bottom,
scrollbar_info: merged_scrollbar_info.clone(),
});
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<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut TextLayoutCache,
node_index: usize,
content_box_pos: LogicalPosition,
available_size: LogicalSize,
calculated_positions: &mut super::PositionVec,
reflow_needed_for_scrollbars: &mut bool,
float_cache: &mut BTreeMap<usize, fc::FloatingContext>,
) -> Result<()> {
let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
let children: Vec<usize> = node.children.clone();
let fc = node.formatting_context.clone();
if matches!(fc, FormattingContext::Flex | FormattingContext::Grid) {
for &child_index in &children {
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let child_rel_pos = child_node.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 child_content_box = LogicalPosition::new(
child_abs_pos.x
+ child_node.box_props.border.left
+ child_node.box_props.padding.left,
child_abs_pos.y
+ child_node.box_props.border.top
+ child_node.box_props.padding.top,
);
let child_inner_size = child_node.box_props.inner_size(
child_node.used_size.unwrap_or_default(),
LayoutWritingMode::HorizontalTb,
);
position_flex_child_descendants(
ctx,
tree,
text_cache,
child_index,
child_content_box,
child_inner_size,
calculated_positions,
reflow_needed_for_scrollbars,
float_cache,
)?;
}
} else {
let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
let children: Vec<usize> = node.children.clone();
for &child_index in &children {
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let child_rel_pos = child_node.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 child_content_box = LogicalPosition::new(
child_abs_pos.x
+ child_node.box_props.border.left
+ child_node.box_props.padding.left,
child_abs_pos.y
+ child_node.box_props.border.top
+ child_node.box_props.padding.top,
);
let child_inner_size = child_node.box_props.inner_size(
child_node.used_size.unwrap_or_default(),
LayoutWritingMode::HorizontalTb,
);
position_flex_child_descendants(
ctx,
tree,
text_cache,
child_index,
child_content_box,
child_inner_size,
calculated_positions,
reflow_needed_for_scrollbars,
float_cache,
)?;
}
}
Ok(())
}
fn set_static_positions_recursive<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
_text_cache: &mut TextLayoutCache,
node_index: usize,
parent_content_box_pos: LogicalPosition,
calculated_positions: &mut super::PositionVec,
) -> Result<()> {
let out_of_flow_children: Vec<(usize, Option<NodeId>)> = {
let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
node.children
.iter()
.filter_map(|&child_index| {
if super::pos_contains(calculated_positions, child_index) {
None
} else {
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 {
if let Some(child_dom_id) = child_dom_id_opt {
let position_type = get_position_type(ctx.styled_dom, Some(child_dom_id));
if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
super::pos_set(calculated_positions, child_index, parent_content_box_pos);
set_static_positions_recursive(
ctx,
tree,
_text_cache,
child_index,
parent_content_box_pos,
calculated_positions,
)?;
}
}
}
Ok(())
}
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 => {
true
}
LayoutHeight::Calc(_) => {
false
}
},
}
}
fn apply_content_based_height(
mut used_size: LogicalSize,
content_size: LogicalSize,
tree: &LayoutTree,
node_index: usize,
writing_mode: LayoutWritingMode,
) -> LogicalSize {
let node_props = &tree.get(node_index).unwrap().box_props;
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);
used_size
}
fn hash_styled_node_data(dom: &StyledDom, node_id: NodeId) -> u64 {
let mut hasher = DefaultHasher::new();
if let Some(styled_node) = dom.styled_nodes.as_container().get(node_id) {
styled_node.styled_node_state.hash(&mut hasher);
}
if let Some(node_data) = dom.node_data.as_container().get(node_id) {
node_data.get_node_type().hash(&mut hasher);
}
hasher.finish()
}
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())
}
pub fn compute_counters(
styled_dom: &StyledDom,
tree: &LayoutTree,
counters: &mut BTreeMap<(usize, String), i32>,
) {
use std::collections::HashMap;
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,
);
}
fn compute_counters_recursive(
styled_dom: &StyledDom,
tree: &LayoutTree,
node_idx: usize,
counters: &mut BTreeMap<(usize, String), i32>,
counter_stacks: &mut std::collections::HashMap<String, Vec<i32>>,
scope_stack: &mut Vec<Vec<String>>,
) {
let node = match tree.get(node_idx) {
Some(n) => n,
None => return,
};
if node.pseudo_element.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 dom_id = match node.dom_node_id {
Some(id) => id,
None => {
for &child_idx in &node.children {
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 counter_reset = cache
.get_counter_reset(node_data, &dom_id, node_state)
.and_then(|v| v.get_property());
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 = cache
.get_counter_increment(node_data, &dom_id, node_state)
.and_then(|v| v.get_property());
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.clone()).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.clone()).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 &node.children {
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();
}
}
}
}