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, LayoutFlexWrap, LayoutHeight, LayoutJustifyContent, 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_justify_content, get_overflow_x,
get_overflow_y, get_scrollbar_gutter_property, get_text_align, get_white_space_property, get_wrap, 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(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() < 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_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() < 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]
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(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 {
pub 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 memory_report(&self) -> Solver3CacheMemoryReport {
let tree_report = self.tree.as_ref().map(|t| t.memory_report());
let tree_bytes = tree_report.as_ref().map(|r| r.total_bytes()).unwrap_or(0);
let mut cache_map_bytes = self.cache_map.entries.capacity()
* core::mem::size_of::<NodeCache>();
for e in &self.cache_map.entries {
if let Some(le) = &e.layout_entry {
cache_map_bytes += le.child_positions.capacity()
* core::mem::size_of::<(usize, LogicalPosition)>();
}
}
Solver3CacheMemoryReport {
tree_bytes,
tree_report,
calculated_positions_bytes: self.calculated_positions.len()
* core::mem::size_of::<LogicalPosition>(),
previous_positions_bytes: self.previous_positions.len()
* core::mem::size_of::<LogicalPosition>(),
scroll_ids_bytes: self.scroll_ids.len()
* (core::mem::size_of::<usize>() + core::mem::size_of::<u64>()),
scroll_id_to_node_id_bytes: self.scroll_id_to_node_id.len()
* (core::mem::size_of::<u64>() + core::mem::size_of::<NodeId>()),
counters_bytes: self.counters.iter().map(|((_, name), _)| {
core::mem::size_of::<(usize, String)>()
+ core::mem::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 {
pub fn is_clean(&self) -> bool {
self.intrinsic_dirty.is_empty()
&& self.layout_roots.is_empty()
&& self.paint_dirty.is_empty()
}
pub fn needs_layout(&self) -> bool {
!self.intrinsic_dirty.is_empty() || !self.layout_roots.is_empty()
}
pub fn needs_paint_only(&self) -> bool {
!self.needs_layout() && !self.paint_dirty.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,
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;
};
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;
}
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,
tree: &LayoutTree,
layout_roots: &BTreeSet<usize>,
calculated_positions: &mut super::PositionVec,
) {
let parent_node = match tree.get(parent_idx) {
Some(n) => n,
None => 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.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 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 child_node = match tree.get(child_idx) {
Some(n) => n,
None => 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;
}
}
}
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) {
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: &azul_core::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,
_ => azul_css::props::layout::display::LayoutDisplay::Block,
};
let is_table_structural = matches!(
parent_display,
azul_css::props::layout::display::LayoutDisplay::Table
| azul_css::props::layout::display::LayoutDisplay::InlineTable
| azul_css::props::layout::display::LayoutDisplay::TableRowGroup
| azul_css::props::layout::display::LayoutDisplay::TableHeaderGroup
| azul_css::props::layout::display::LayoutDisplay::TableFooterGroup
| azul_css::props::layout::display::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,
_ => azul_css::props::layout::display::LayoutDisplay::Block,
};
if matches!(display, azul_css::props::layout::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 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);
unsafe { core::ptr::write_volatile(0x400AC as *mut u32, 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) | 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| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')) {
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_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 = match old_cold {
None => {
drop(crate::probe::Probe::span("fp_new_node"));
DirtyFlag::Layout },
Some(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"));
static FP_DUMP_ENABLED: std::sync::OnceLock<bool> =
std::sync::OnceLock::new();
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() {
unsafe { core::ptr::write_volatile(0x400A8 as *mut u32, 0xBB00_0001u32); }
new_tree_builder.create_node_from_dom(
styled_dom,
new_dom_id,
new_parent_idx,
debug_messages,
)
} else {
unsafe { core::ptr::write_volatile(0x400A8 as *mut u32, 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
};
unsafe { core::ptr::write_volatile(0x400BC as *mut u32, 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,
_ => azul_css::props::layout::display::LayoutDisplay::Block,
};
if matches!(parent_display,
azul_css::props::layout::display::LayoutDisplay::Table
| azul_css::props::layout::display::LayoutDisplay::InlineTable
| azul_css::props::layout::display::LayoutDisplay::TableRowGroup
| azul_css::props::layout::display::LayoutDisplay::TableHeaderGroup
| azul_css::props::layout::display::LayoutDisplay::TableFooterGroup
| azul_css::props::layout::display::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));
if !has_block_child {
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<_>>()
)));
}
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<_>>()
)));
}
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.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.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;
};
match &warm_node.scrollbar_info {
None => scrollbar_info.needs_reflow(),
Some(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 merge_scrollbar_info(
_tree: &LayoutTree,
_node_index: usize,
new_info: &ScrollbarRequirements,
) -> ScrollbarRequirements {
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: &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(|n| format!("{:?}", n.node_type))
.unwrap_or_else(|| "Unknown".to_string());
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(|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.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: &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 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.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 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(())
}
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<()> {
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)
.cloned();
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.clone());
}
return Ok(());
}
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.clone());
}
let box_props = tree.get(node_index)
.map(|n| n.box_props.unpack())
.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,
LayoutWritingMode::HorizontalTb,
);
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).map_or(false, |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.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);
}
}
if let Some(warm) = tree.warm_mut(node_index) {
warm.scrollbar_info = Some(merged_scrollbar_info.clone());
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.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 HashMap<usize, fc::FloatingContext>,
) -> Result<()> {
let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
let children: Vec<usize> = tree.children(node_index).to_vec();
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 = 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 = 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(),
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> = 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_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(),
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 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,
) -> LogicalSize {
let node_props = tree.get(node_index).unwrap().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);
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())
}
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,
);
}
fn compute_counters_recursive(
styled_dom: &StyledDom,
tree: &LayoutTree,
node_idx: usize,
counters: &mut HashMap<(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 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 dom_id = match node.dom_node_id {
Some(id) => id,
None => {
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().map_or(true, |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.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 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();
}
}
}
}