use std::{
collections::BTreeMap,
hash::{Hash, Hasher},
sync::{
atomic::{AtomicU32, Ordering},
Arc,
},
};
use crate::text3::cache::UnifiedConstraints;
static IFC_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct IfcId(pub u32);
impl IfcId {
pub fn unique() -> Self {
Self(IFC_ID_COUNTER.fetch_add(1, Ordering::Relaxed))
}
pub fn reset_counter() {
IFC_ID_COUNTER.store(0, Ordering::Relaxed);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct IfcMembership {
pub ifc_id: IfcId,
pub ifc_root_layout_index: usize,
pub run_index: u32,
}
use azul_core::{
dom::{FormattingContext, NodeId, NodeType},
geom::{LogicalPosition, LogicalRect, LogicalSize},
styled_dom::StyledDom,
};
use azul_css::{
corety::LayoutDebugMessage,
css::CssPropertyValue,
format_rust_code::GetHash,
props::{
basic::{
pixel::DEFAULT_FONT_SIZE, PhysicalSize, PixelValue, PropertyContext, ResolutionContext,
},
layout::{
LayoutDisplay, LayoutFloat, LayoutHeight, LayoutMaxHeight, LayoutMaxWidth,
LayoutMinHeight, LayoutMinWidth, LayoutOverflow, LayoutPosition, LayoutWidth,
LayoutWritingMode,
},
property::{CssProperty, CssPropertyType},
style::StyleTextAlign,
},
};
use taffy::{Cache as TaffyCache, Layout, LayoutInput, LayoutOutput};
#[cfg(feature = "text_layout")]
use crate::text3;
use crate::{
debug_log,
font::parsed::ParsedFont,
font_traits::{FontLoaderTrait, ParsedFontTrait, UnifiedLayout},
solver3::{
geometry::{BoxProps, IntrinsicSizes, PositionedRectangle},
getters::{
get_css_height, get_css_max_height, get_css_max_width, get_css_min_height,
get_css_min_width, get_css_width, get_display_property, get_float, get_overflow_x,
get_overflow_y, get_position, get_text_align, get_writing_mode, MultiValue,
},
scrollbar::ScrollbarRequirements,
LayoutContext, Result,
},
text3::cache::AvailableSpace,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum DirtyFlag {
#[default]
None,
Paint,
Layout,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
pub struct SubtreeHash(pub u64);
#[derive(Debug, Clone)]
pub struct InlineItemMetrics {
pub source_node_id: Option<NodeId>,
pub advance_width: f32,
pub line_height_contribution: f32,
pub can_break: bool,
pub line_index: u32,
pub x_offset: f32,
}
#[derive(Debug, Clone)]
pub struct CachedInlineLayout {
pub layout: Arc<UnifiedLayout>,
pub available_width: AvailableSpace,
pub has_floats: bool,
pub constraints: Option<UnifiedConstraints>,
pub item_metrics: Vec<InlineItemMetrics>,
}
impl CachedInlineLayout {
pub fn new(
layout: Arc<UnifiedLayout>,
available_width: AvailableSpace,
has_floats: bool,
) -> Self {
let item_metrics = Self::extract_item_metrics(&layout);
Self {
layout,
available_width,
has_floats,
constraints: None,
item_metrics,
}
}
pub fn new_with_constraints(
layout: Arc<UnifiedLayout>,
available_width: AvailableSpace,
has_floats: bool,
constraints: UnifiedConstraints,
) -> Self {
let item_metrics = Self::extract_item_metrics(&layout);
Self {
layout,
available_width,
has_floats,
constraints: Some(constraints),
item_metrics,
}
}
fn extract_item_metrics(layout: &UnifiedLayout) -> Vec<InlineItemMetrics> {
use crate::text3::cache::{ShapedItem, get_item_vertical_metrics};
layout.items.iter().map(|positioned_item| {
let bounds = positioned_item.item.bounds();
let (ascent, descent) = get_item_vertical_metrics(&positioned_item.item);
let source_node_id = match &positioned_item.item {
ShapedItem::Cluster(c) => c.source_node_id,
ShapedItem::Object { .. }
| ShapedItem::CombinedBlock { .. }
| ShapedItem::Tab { .. }
| ShapedItem::Break { .. } => None,
};
let can_break = !matches!(&positioned_item.item, ShapedItem::Break { .. });
InlineItemMetrics {
source_node_id,
advance_width: bounds.width,
line_height_contribution: ascent + descent,
can_break,
line_index: positioned_item.line_index as u32,
x_offset: positioned_item.position.x,
}
}).collect()
}
pub fn is_valid_for(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
if self.has_floats && !new_has_floats {
return self.width_constraint_matches(new_width);
}
self.width_constraint_matches(new_width)
}
fn width_constraint_matches(&self, new_width: AvailableSpace) -> bool {
match (self.available_width, new_width) {
(AvailableSpace::Definite(old), AvailableSpace::Definite(new)) => {
(old - new).abs() < 0.1
}
(AvailableSpace::MinContent, AvailableSpace::MinContent) => true,
(AvailableSpace::MaxContent, AvailableSpace::MaxContent) => true,
_ => false,
}
}
pub fn should_replace_with(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
if new_has_floats && !self.has_floats {
return true;
}
!self.width_constraint_matches(new_width)
}
#[inline]
pub fn get_layout(&self) -> &Arc<UnifiedLayout> {
&self.layout
}
#[inline]
pub fn clone_layout(&self) -> Arc<UnifiedLayout> {
self.layout.clone()
}
}
#[derive(Debug, Clone)]
pub struct LayoutNode {
pub dom_node_id: Option<NodeId>,
pub pseudo_element: Option<PseudoElement>,
pub is_anonymous: bool,
pub anonymous_type: Option<AnonymousBoxType>,
pub children: Vec<usize>,
pub parent: Option<usize>,
pub dirty_flag: DirtyFlag,
pub unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps,
pub box_props: BoxProps,
pub taffy_cache: TaffyCache, pub node_data_hash: u64,
pub subtree_hash: SubtreeHash,
pub formatting_context: FormattingContext,
pub parent_formatting_context: Option<FormattingContext>,
pub intrinsic_sizes: Option<IntrinsicSizes>,
pub used_size: Option<LogicalSize>,
pub relative_position: Option<LogicalPosition>,
pub baseline: Option<f32>,
pub inline_layout_result: Option<CachedInlineLayout>,
pub escaped_top_margin: Option<f32>,
pub escaped_bottom_margin: Option<f32>,
pub scrollbar_info: Option<ScrollbarRequirements>,
pub overflow_content_size: Option<LogicalSize>,
pub ifc_id: Option<IfcId>,
pub ifc_membership: Option<IfcMembership>,
pub computed_style: ComputedLayoutStyle,
}
#[derive(Debug, Clone, Default)]
pub struct ComputedLayoutStyle {
pub display: LayoutDisplay,
pub position: LayoutPosition,
pub float: LayoutFloat,
pub overflow_x: LayoutOverflow,
pub overflow_y: LayoutOverflow,
pub writing_mode: azul_css::props::layout::LayoutWritingMode,
pub width: Option<azul_css::props::layout::LayoutWidth>,
pub height: Option<azul_css::props::layout::LayoutHeight>,
pub min_width: Option<azul_css::props::layout::LayoutMinWidth>,
pub min_height: Option<azul_css::props::layout::LayoutMinHeight>,
pub max_width: Option<azul_css::props::layout::LayoutMaxWidth>,
pub max_height: Option<azul_css::props::layout::LayoutMaxHeight>,
pub text_align: azul_css::props::style::StyleTextAlign,
}
impl LayoutNode {
pub fn resolve_box_props_with_containing_block(
&mut self,
containing_block: LogicalSize,
viewport_size: LogicalSize,
element_font_size: f32,
root_font_size: f32,
) {
let params = crate::solver3::geometry::ResolutionParams {
containing_block,
viewport_size,
element_font_size,
root_font_size,
};
self.box_props = self.unresolved_box_props.resolve(¶ms);
}
pub fn get_content_size(&self) -> LogicalSize {
if let Some(content_size) = self.overflow_content_size {
return content_size;
}
let mut content_size = self.used_size.unwrap_or_default();
if let Some(ref cached_layout) = self.inline_layout_result {
let text_layout = &cached_layout.layout;
let mut max_x: f32 = 0.0;
let mut max_y: f32 = 0.0;
for positioned_item in &text_layout.items {
let item_bounds = positioned_item.item.bounds();
let item_right = positioned_item.position.x + item_bounds.width;
let item_bottom = positioned_item.position.y + item_bounds.height;
max_x = max_x.max(item_right);
max_y = max_y.max(item_bottom);
}
content_size.width = content_size.width.max(max_x);
content_size.height = content_size.height.max(max_y);
}
content_size
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PseudoElement {
Marker,
Before,
After,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AnonymousBoxType {
InlineWrapper,
ListItemMarker,
TableWrapper,
TableRowGroup,
TableRow,
TableCell,
}
#[derive(Debug, Clone)]
pub struct LayoutTree {
pub nodes: Vec<LayoutNode>,
pub root: usize,
pub dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
}
impl LayoutTree {
pub fn get(&self, index: usize) -> Option<&LayoutNode> {
self.nodes.get(index)
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
self.nodes.get_mut(index)
}
pub fn root_node(&self) -> &LayoutNode {
&self.nodes[self.root]
}
pub fn resolve_box_props(
&mut self,
node_index: usize,
containing_block: LogicalSize,
viewport_size: LogicalSize,
element_font_size: f32,
root_font_size: f32,
) {
if let Some(node) = self.nodes.get_mut(node_index) {
node.resolve_box_props_with_containing_block(
containing_block,
viewport_size,
element_font_size,
root_font_size,
);
}
}
pub fn mark_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
if flag == DirtyFlag::None {
return;
}
let mut current_index = Some(start_index);
while let Some(index) = current_index {
if let Some(node) = self.get_mut(index) {
if node.dirty_flag >= flag {
break;
}
node.dirty_flag = flag;
current_index = node.parent;
} else {
break;
}
}
}
pub fn mark_subtree_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
if flag == DirtyFlag::None {
return;
}
let mut stack = vec![start_index];
while let Some(index) = stack.pop() {
if let Some(node) = self.get_mut(index) {
if node.dirty_flag < flag {
node.dirty_flag = flag;
}
stack.extend_from_slice(&node.children);
}
}
}
pub fn clear_all_dirty_flags(&mut self) {
for node in &mut self.nodes {
node.dirty_flag = DirtyFlag::None;
}
}
pub fn get_inline_layout_for_node(&self, layout_index: usize) -> Option<&std::sync::Arc<UnifiedLayout>> {
let layout_node = self.nodes.get(layout_index)?;
if let Some(cached) = &layout_node.inline_layout_result {
return Some(cached.get_layout());
}
if let Some(ifc_membership) = &layout_node.ifc_membership {
let ifc_root_node = self.nodes.get(ifc_membership.ifc_root_layout_index)?;
if let Some(cached) = &ifc_root_node.inline_layout_result {
return Some(cached.get_layout());
}
}
None
}
}
pub fn generate_layout_tree<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
) -> Result<LayoutTree> {
let mut builder = LayoutTreeBuilder::new(ctx.viewport_size);
let root_id = ctx
.styled_dom
.root
.into_crate_internal()
.unwrap_or(NodeId::ZERO);
let root_index =
builder.process_node(ctx.styled_dom, root_id, None, &mut ctx.debug_messages)?;
let layout_tree = builder.build(root_index);
debug_log!(
ctx,
"Generated layout tree with {} nodes (incl. anonymous)",
layout_tree.nodes.len()
);
Ok(layout_tree)
}
pub struct LayoutTreeBuilder {
nodes: Vec<LayoutNode>,
dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
viewport_size: LogicalSize,
}
impl LayoutTreeBuilder {
pub fn new(viewport_size: LogicalSize) -> Self {
Self {
nodes: Vec::new(),
dom_to_layout: BTreeMap::new(),
viewport_size,
}
}
pub fn get(&self, index: usize) -> Option<&LayoutNode> {
self.nodes.get(index)
}
pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
self.nodes.get_mut(index)
}
pub fn process_node(
&mut self,
styled_dom: &StyledDom,
dom_id: NodeId,
parent_idx: Option<usize>,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> Result<usize> {
let node_data = &styled_dom.node_data.as_container()[dom_id];
let node_idx = self.create_node_from_dom(styled_dom, dom_id, parent_idx, debug_messages)?;
let display_type = get_display_type(styled_dom, dom_id);
if display_type == LayoutDisplay::ListItem {
self.create_marker_pseudo_element(styled_dom, dom_id, node_idx);
}
match display_type {
LayoutDisplay::Block
| LayoutDisplay::InlineBlock
| LayoutDisplay::FlowRoot
| LayoutDisplay::ListItem => {
self.process_block_children(styled_dom, dom_id, node_idx, debug_messages)?
}
LayoutDisplay::Table => {
self.process_table_children(styled_dom, dom_id, node_idx, debug_messages)?
}
LayoutDisplay::TableRowGroup => {
self.process_table_row_group_children(styled_dom, dom_id, node_idx, debug_messages)?
}
LayoutDisplay::TableRow => {
self.process_table_row_children(styled_dom, dom_id, node_idx, debug_messages)?
}
_ => {
let children: Vec<NodeId> = dom_id
.az_children(&styled_dom.node_hierarchy.as_container())
.filter(|&child_id| {
if get_display_type(styled_dom, child_id) == LayoutDisplay::None {
return false;
}
let node_data = &styled_dom.node_data.as_container()[child_id];
if let NodeType::Text(text) = node_data.get_node_type() {
return !text.as_str().trim().is_empty();
}
true
})
.collect();
for child_dom_id in children {
self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
}
}
}
Ok(node_idx)
}
fn process_block_children(
&mut self,
styled_dom: &StyledDom,
parent_dom_id: NodeId,
parent_idx: usize,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> Result<()> {
let children: Vec<NodeId> = parent_dom_id
.az_children(&styled_dom.node_hierarchy.as_container())
.filter(|&child_id| get_display_type(styled_dom, child_id) != LayoutDisplay::None)
.collect();
if let Some(msgs) = debug_messages.as_mut() {
msgs.push(LayoutDebugMessage::info(format!(
"[process_block_children] DOM node {} has {} children: {:?}",
parent_dom_id.index(),
children.len(),
children.iter().map(|c| c.index()).collect::<Vec<_>>()
)));
}
let has_block_child = children.iter().any(|&id| is_block_level(styled_dom, id));
if let Some(msgs) = debug_messages.as_mut() {
msgs.push(LayoutDebugMessage::info(format!(
"[process_block_children] has_block_child={}, children display types: {:?}",
has_block_child,
children
.iter()
.map(|c| {
let dt = get_display_type(styled_dom, *c);
let is_block = is_block_level(styled_dom, *c);
format!("{}:{:?}(block={})", c.index(), dt, is_block)
})
.collect::<Vec<_>>()
)));
}
if !has_block_child {
if let Some(msgs) = debug_messages.as_mut() {
msgs.push(LayoutDebugMessage::info(format!(
"[process_block_children] All inline, processing {} children directly",
children.len()
)));
}
for child_id in children {
self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
}
return Ok(());
}
let mut inline_run = Vec::new();
for child_id in children {
if is_block_level(styled_dom, child_id) {
if !inline_run.is_empty() {
if let Some(msgs) = debug_messages.as_mut() {
msgs.push(LayoutDebugMessage::info(format!(
"[process_block_children] Creating anon wrapper for inline run: {:?}",
inline_run
.iter()
.map(|c: &NodeId| c.index())
.collect::<Vec<_>>()
)));
}
let anon_idx = self.create_anonymous_node(
parent_idx,
AnonymousBoxType::InlineWrapper,
FormattingContext::Block {
establishes_new_context: true,
},
);
for inline_child_id in inline_run.drain(..) {
self.process_node(
styled_dom,
inline_child_id,
Some(anon_idx),
debug_messages,
)?;
}
}
if let Some(msgs) = debug_messages.as_mut() {
msgs.push(LayoutDebugMessage::info(format!(
"[process_block_children] Processing block child DOM {}",
child_id.index()
)));
}
self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
} else {
inline_run.push(child_id);
}
}
if !inline_run.is_empty() {
if let Some(msgs) = debug_messages.as_mut() {
msgs.push(LayoutDebugMessage::info(format!(
"[process_block_children] Creating anon wrapper for remaining inline run: {:?}",
inline_run.iter().map(|c| c.index()).collect::<Vec<_>>()
)));
}
let anon_idx = self.create_anonymous_node(
parent_idx,
AnonymousBoxType::InlineWrapper,
FormattingContext::Block {
establishes_new_context: true, },
);
for inline_child_id in inline_run {
self.process_node(styled_dom, inline_child_id, Some(anon_idx), debug_messages)?;
}
}
Ok(())
}
fn process_table_children(
&mut self,
styled_dom: &StyledDom,
parent_dom_id: NodeId,
parent_idx: usize,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> Result<()> {
let parent_display = get_display_type(styled_dom, parent_dom_id);
let mut row_children = Vec::new();
for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
continue;
}
let child_display = get_display_type(styled_dom, child_id);
if child_display == LayoutDisplay::TableCell {
row_children.push(child_id);
} else {
if !row_children.is_empty() {
let anon_row_idx = self.create_anonymous_node(
parent_idx,
AnonymousBoxType::TableRow,
FormattingContext::TableRow,
);
for cell_id in row_children.drain(..) {
self.process_node(styled_dom, cell_id, Some(anon_row_idx), debug_messages)?;
}
}
self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
}
}
if !row_children.is_empty() {
let anon_row_idx = self.create_anonymous_node(
parent_idx,
AnonymousBoxType::TableRow,
FormattingContext::TableRow,
);
for cell_id in row_children {
self.process_node(styled_dom, cell_id, Some(anon_row_idx), debug_messages)?;
}
}
Ok(())
}
fn process_table_row_group_children(
&mut self,
styled_dom: &StyledDom,
parent_dom_id: NodeId,
parent_idx: usize,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> Result<()> {
self.process_table_children(styled_dom, parent_dom_id, parent_idx, debug_messages)
}
fn process_table_row_children(
&mut self,
styled_dom: &StyledDom,
parent_dom_id: NodeId,
parent_idx: usize,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> Result<()> {
let parent_display = get_display_type(styled_dom, parent_dom_id);
for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
continue;
}
let child_display = get_display_type(styled_dom, child_id);
if child_display == LayoutDisplay::TableCell {
self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
} else {
let anon_cell_idx = self.create_anonymous_node(
parent_idx,
AnonymousBoxType::TableCell,
FormattingContext::Block {
establishes_new_context: true,
},
);
self.process_node(styled_dom, child_id, Some(anon_cell_idx), debug_messages)?;
}
}
Ok(())
}
pub fn create_anonymous_node(
&mut self,
parent: usize,
anon_type: AnonymousBoxType,
fc: FormattingContext,
) -> usize {
let index = self.nodes.len();
let parent_fc = self.nodes.get(parent).map(|n| n.formatting_context.clone());
self.nodes.push(LayoutNode {
dom_node_id: None,
pseudo_element: None,
parent: Some(parent),
formatting_context: fc,
parent_formatting_context: parent_fc,
unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
box_props: BoxProps::default(),
taffy_cache: TaffyCache::new(),
is_anonymous: true,
anonymous_type: Some(anon_type),
children: Vec::new(),
dirty_flag: DirtyFlag::Layout,
node_data_hash: 0,
subtree_hash: SubtreeHash(0),
intrinsic_sizes: None,
used_size: None,
relative_position: None,
baseline: None,
inline_layout_result: None,
escaped_top_margin: None,
escaped_bottom_margin: None,
scrollbar_info: None,
overflow_content_size: None,
ifc_id: None,
ifc_membership: None,
computed_style: ComputedLayoutStyle::default(),
});
self.nodes[parent].children.push(index);
index
}
pub fn create_marker_pseudo_element(
&mut self,
styled_dom: &StyledDom,
list_item_dom_id: NodeId,
list_item_idx: usize,
) -> usize {
let index = self.nodes.len();
let parent_fc = self
.nodes
.get(list_item_idx)
.map(|n| n.formatting_context.clone());
self.nodes.push(LayoutNode {
dom_node_id: Some(list_item_dom_id),
pseudo_element: Some(PseudoElement::Marker),
parent: Some(list_item_idx),
formatting_context: FormattingContext::Inline,
parent_formatting_context: parent_fc,
unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
box_props: BoxProps::default(),
taffy_cache: TaffyCache::new(),
is_anonymous: false,
anonymous_type: None,
children: Vec::new(),
dirty_flag: DirtyFlag::Layout,
node_data_hash: 0,
subtree_hash: SubtreeHash(0),
intrinsic_sizes: None,
used_size: None,
relative_position: None,
baseline: None,
inline_layout_result: None,
escaped_top_margin: None,
escaped_bottom_margin: None,
scrollbar_info: None,
overflow_content_size: None,
ifc_id: None,
ifc_membership: None,
computed_style: ComputedLayoutStyle::default(),
});
self.nodes[list_item_idx].children.insert(0, index);
self.dom_to_layout
.entry(list_item_dom_id)
.or_default()
.push(index);
index
}
pub fn create_node_from_dom(
&mut self,
styled_dom: &StyledDom,
dom_id: NodeId,
parent: Option<usize>,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> Result<usize> {
let index = self.nodes.len();
let parent_fc =
parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
let collected = collect_box_props(styled_dom, dom_id, debug_messages, self.viewport_size);
self.nodes.push(LayoutNode {
dom_node_id: Some(dom_id),
pseudo_element: None,
parent,
formatting_context: determine_formatting_context(styled_dom, dom_id),
parent_formatting_context: parent_fc,
unresolved_box_props: collected.unresolved,
box_props: collected.resolved,
taffy_cache: TaffyCache::new(),
is_anonymous: false,
anonymous_type: None,
children: Vec::new(),
dirty_flag: DirtyFlag::Layout,
node_data_hash: hash_node_data(styled_dom, dom_id),
subtree_hash: SubtreeHash(0),
intrinsic_sizes: None,
used_size: None,
relative_position: None,
baseline: None,
inline_layout_result: None,
escaped_top_margin: None,
escaped_bottom_margin: None,
scrollbar_info: None,
overflow_content_size: None,
ifc_id: None,
ifc_membership: None,
computed_style: compute_layout_style(styled_dom, dom_id),
});
if let Some(p) = parent {
self.nodes[p].children.push(index);
}
self.dom_to_layout.entry(dom_id).or_default().push(index);
Ok(index)
}
pub fn clone_node_from_old(&mut self, old_node: &LayoutNode, parent: Option<usize>) -> usize {
let index = self.nodes.len();
let mut new_node = old_node.clone();
new_node.parent = parent;
new_node.parent_formatting_context =
parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
new_node.children = Vec::new();
new_node.dirty_flag = DirtyFlag::None;
self.nodes.push(new_node);
if let Some(p) = parent {
self.nodes[p].children.push(index);
}
if let Some(dom_id) = old_node.dom_node_id {
self.dom_to_layout.entry(dom_id).or_default().push(index);
}
index
}
pub fn build(self, root_idx: usize) -> LayoutTree {
LayoutTree {
nodes: self.nodes,
root: root_idx,
dom_to_layout: self.dom_to_layout,
}
}
}
pub fn is_block_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
matches!(
get_display_type(styled_dom, node_id),
LayoutDisplay::Block
| LayoutDisplay::FlowRoot
| LayoutDisplay::Flex
| LayoutDisplay::Grid
| LayoutDisplay::Table
| LayoutDisplay::TableCaption
| LayoutDisplay::TableRow
| LayoutDisplay::TableRowGroup
| LayoutDisplay::ListItem
)
}
fn is_inline_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
let node_data = &styled_dom.node_data.as_container()[node_id];
if matches!(node_data.get_node_type(), NodeType::Text(_)) {
return true;
}
matches!(
get_display_type(styled_dom, node_id),
LayoutDisplay::Inline
| LayoutDisplay::InlineBlock
| LayoutDisplay::InlineTable
| LayoutDisplay::InlineFlex
| LayoutDisplay::InlineGrid
)
}
fn has_only_inline_children(styled_dom: &StyledDom, node_id: NodeId) -> bool {
let hierarchy = styled_dom.node_hierarchy.as_container();
let node_hier = match hierarchy.get(node_id) {
Some(n) => n,
None => {
return false;
}
};
let mut current_child = node_hier.first_child_id(node_id);
if current_child.is_none() {
return false;
}
while let Some(child_id) = current_child {
let is_inline = is_inline_level(styled_dom, child_id);
if !is_inline {
return false;
}
if let Some(child_hier) = hierarchy.get(child_id) {
current_child = child_hier.next_sibling_id();
} else {
break;
}
}
true
}
fn compute_layout_style(styled_dom: &StyledDom, dom_id: NodeId) -> ComputedLayoutStyle {
let styled_node_state = styled_dom
.styled_nodes
.as_container()
.get(dom_id)
.map(|n| n.styled_node_state.clone())
.unwrap_or_default();
let display = match get_display_property(styled_dom, Some(dom_id)) {
MultiValue::Exact(d) => d,
MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => LayoutDisplay::Block,
};
let position = get_position(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
let float = get_float(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
let overflow_x = get_overflow_x(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
let overflow_y = get_overflow_y(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
let writing_mode = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
let text_align = get_text_align(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
let width = match get_css_width(styled_dom, dom_id, &styled_node_state) {
MultiValue::Exact(w) => Some(w),
_ => None,
};
let height = match get_css_height(styled_dom, dom_id, &styled_node_state) {
MultiValue::Exact(h) => Some(h),
_ => None,
};
let min_width = match get_css_min_width(styled_dom, dom_id, &styled_node_state) {
MultiValue::Exact(v) => Some(v),
_ => None,
};
let min_height = match get_css_min_height(styled_dom, dom_id, &styled_node_state) {
MultiValue::Exact(v) => Some(v),
_ => None,
};
let max_width = match get_css_max_width(styled_dom, dom_id, &styled_node_state) {
MultiValue::Exact(v) => Some(v),
_ => None,
};
let max_height = match get_css_max_height(styled_dom, dom_id, &styled_node_state) {
MultiValue::Exact(v) => Some(v),
_ => None,
};
ComputedLayoutStyle {
display,
position,
float,
overflow_x,
overflow_y,
writing_mode,
width,
height,
min_width,
min_height,
max_width,
max_height,
text_align,
}
}
fn hash_node_data(dom: &StyledDom, node_id: NodeId) -> u64 {
let mut hasher = std::hash::DefaultHasher::new();
if let Some(styled_node) = dom.node_data.as_container().get(node_id) {
styled_node.get_hash().hash(&mut hasher);
}
hasher.finish()
}
fn get_element_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
let node_state = styled_dom
.styled_nodes
.as_container()
.get(dom_id)
.map(|n| &n.styled_node_state)
.cloned()
.unwrap_or_default();
crate::solver3::getters::get_element_font_size(styled_dom, dom_id, &node_state)
}
fn get_parent_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
styled_dom
.node_hierarchy
.as_container()
.get(dom_id)
.and_then(|node| node.parent_id())
.map(|parent_id| get_element_font_size(styled_dom, parent_id))
.unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE)
}
fn get_root_font_size(styled_dom: &StyledDom) -> f32 {
get_element_font_size(styled_dom, NodeId::new(0))
}
fn create_resolution_context(
styled_dom: &StyledDom,
dom_id: NodeId,
containing_block_size: Option<azul_css::props::basic::PhysicalSize>,
viewport_size: LogicalSize,
) -> azul_css::props::basic::ResolutionContext {
let element_font_size = get_element_font_size(styled_dom, dom_id);
let parent_font_size = get_parent_font_size(styled_dom, dom_id);
let root_font_size = get_root_font_size(styled_dom);
ResolutionContext {
element_font_size,
parent_font_size,
root_font_size,
containing_block_size: containing_block_size.unwrap_or(PhysicalSize::new(0.0, 0.0)),
element_size: None, viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
}
}
struct CollectedBoxProps {
unresolved: crate::solver3::geometry::UnresolvedBoxProps,
resolved: BoxProps,
}
fn collect_box_props(
styled_dom: &StyledDom,
dom_id: NodeId,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
viewport_size: LogicalSize,
) -> CollectedBoxProps {
use crate::solver3::geometry::{UnresolvedBoxProps, UnresolvedEdge, UnresolvedMargin};
use crate::solver3::getters::*;
let node_data = &styled_dom.node_data.as_container()[dom_id];
let node_state = styled_dom
.styled_nodes
.as_container()
.get(dom_id)
.map(|n| &n.styled_node_state)
.cloned()
.unwrap_or_default();
let context = create_resolution_context(styled_dom, dom_id, None, viewport_size);
let margin_top_mv = get_css_margin_top(styled_dom, dom_id, &node_state);
let margin_right_mv = get_css_margin_right(styled_dom, dom_id, &node_state);
let margin_bottom_mv = get_css_margin_bottom(styled_dom, dom_id, &node_state);
let margin_left_mv = get_css_margin_left(styled_dom, dom_id, &node_state);
let to_unresolved_margin = |mv: &MultiValue<PixelValue>| -> UnresolvedMargin {
match mv {
MultiValue::Auto => UnresolvedMargin::Auto,
MultiValue::Exact(pv) => UnresolvedMargin::Length(*pv),
_ => UnresolvedMargin::Zero,
}
};
let unresolved_margin = UnresolvedEdge {
top: to_unresolved_margin(&margin_top_mv),
right: to_unresolved_margin(&margin_right_mv),
bottom: to_unresolved_margin(&margin_bottom_mv),
left: to_unresolved_margin(&margin_left_mv),
};
let padding_top_mv = get_css_padding_top(styled_dom, dom_id, &node_state);
let padding_right_mv = get_css_padding_right(styled_dom, dom_id, &node_state);
let padding_bottom_mv = get_css_padding_bottom(styled_dom, dom_id, &node_state);
let padding_left_mv = get_css_padding_left(styled_dom, dom_id, &node_state);
let to_pixel_value = |mv: MultiValue<PixelValue>| -> PixelValue {
match mv {
MultiValue::Exact(pv) => pv,
_ => PixelValue::const_px(0),
}
};
let unresolved_padding = UnresolvedEdge {
top: to_pixel_value(padding_top_mv),
right: to_pixel_value(padding_right_mv),
bottom: to_pixel_value(padding_bottom_mv),
left: to_pixel_value(padding_left_mv),
};
let border_top_mv = get_css_border_top_width(styled_dom, dom_id, &node_state);
let border_right_mv = get_css_border_right_width(styled_dom, dom_id, &node_state);
let border_bottom_mv = get_css_border_bottom_width(styled_dom, dom_id, &node_state);
let border_left_mv = get_css_border_left_width(styled_dom, dom_id, &node_state);
let unresolved_border = UnresolvedEdge {
top: to_pixel_value(border_top_mv),
right: to_pixel_value(border_right_mv),
bottom: to_pixel_value(border_bottom_mv),
left: to_pixel_value(border_left_mv),
};
let unresolved = UnresolvedBoxProps {
margin: unresolved_margin,
padding: unresolved_padding,
border: unresolved_border,
};
let params = crate::solver3::geometry::ResolutionParams {
containing_block: viewport_size,
viewport_size,
element_font_size: context.parent_font_size,
root_font_size: context.root_font_size,
};
let resolved = unresolved.resolve(¶ms);
if let Some(msgs) = debug_messages.as_mut() {
let has_vh = match &unresolved_margin.top {
UnresolvedMargin::Length(pv) => pv.metric == azul_css::props::basic::SizeMetric::Vh,
_ => false,
};
if has_vh || resolved.margin.top > 0.0 || resolved.margin.left > 0.0 {
msgs.push(LayoutDebugMessage::box_props(format!(
"NodeId {:?} ({:?}): unresolved_margin_top={:?}, resolved_margin_top={:.2}, viewport_size={:?}",
dom_id, node_data.node_type,
unresolved_margin.top,
resolved.margin.top,
viewport_size
)));
}
}
if let Some(msgs) = debug_messages.as_mut() {
msgs.push(LayoutDebugMessage::box_props(format!(
"NodeId {:?} ({:?}): margin_auto: left={}, right={}, top={}, bottom={} | margin_left={:?}",
dom_id, node_data.node_type,
resolved.margin_auto.left, resolved.margin_auto.right,
resolved.margin_auto.top, resolved.margin_auto.bottom,
unresolved_margin.left
)));
}
if matches!(node_data.node_type, azul_core::dom::NodeType::Body) {
if let Some(msgs) = debug_messages.as_mut() {
msgs.push(LayoutDebugMessage::box_props(format!(
"Body margin resolved: top={:.2}, right={:.2}, bottom={:.2}, left={:.2}",
resolved.margin.top, resolved.margin.right,
resolved.margin.bottom, resolved.margin.left
)));
}
}
CollectedBoxProps { unresolved, resolved }
}
fn is_whitespace_only_text(styled_dom: &StyledDom, node_id: NodeId) -> bool {
let binding = styled_dom.node_data.as_container();
let node_data = binding.get(node_id);
if let Some(data) = node_data {
if let NodeType::Text(text) = data.get_node_type() {
return text.chars().all(|c| c.is_whitespace());
}
}
false
}
fn should_skip_for_table_structure(
styled_dom: &StyledDom,
node_id: NodeId,
parent_display: LayoutDisplay,
) -> bool {
matches!(
parent_display,
LayoutDisplay::Table
| LayoutDisplay::TableRowGroup
| LayoutDisplay::TableHeaderGroup
| LayoutDisplay::TableFooterGroup
| LayoutDisplay::TableRow
) && is_whitespace_only_text(styled_dom, node_id)
}
fn needs_table_parent_wrapper(
styled_dom: &StyledDom,
node_id: NodeId,
parent_display: LayoutDisplay,
) -> Option<AnonymousBoxType> {
let child_display = get_display_type(styled_dom, node_id);
if child_display == LayoutDisplay::TableCell {
match parent_display {
LayoutDisplay::TableRow
| LayoutDisplay::TableRowGroup
| LayoutDisplay::TableHeaderGroup
| LayoutDisplay::TableFooterGroup => {
None
}
_ => Some(AnonymousBoxType::TableRow),
}
}
else if matches!(child_display, LayoutDisplay::TableRow) {
match parent_display {
LayoutDisplay::Table
| LayoutDisplay::TableRowGroup
| LayoutDisplay::TableHeaderGroup
| LayoutDisplay::TableFooterGroup => {
None }
_ => Some(AnonymousBoxType::TableWrapper),
}
}
else if matches!(
child_display,
LayoutDisplay::TableRowGroup
| LayoutDisplay::TableHeaderGroup
| LayoutDisplay::TableFooterGroup
) {
match parent_display {
LayoutDisplay::Table => None,
_ => Some(AnonymousBoxType::TableWrapper),
}
} else {
None
}
}
pub fn get_display_type(styled_dom: &StyledDom, node_id: NodeId) -> LayoutDisplay {
use crate::solver3::getters::get_display_property;
get_display_property(styled_dom, Some(node_id)).unwrap_or(LayoutDisplay::Inline)
}
fn establishes_new_block_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> bool {
let display = get_display_type(styled_dom, node_id);
if matches!(
display,
LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::FlowRoot
) {
return true;
}
if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
let overflow_x = get_overflow_x(styled_dom, node_id, &styled_node.styled_node_state);
if !overflow_x.is_visible_or_clip() {
return true;
}
let overflow_y = get_overflow_y(styled_dom, node_id, &styled_node.styled_node_state);
if !overflow_y.is_visible_or_clip() {
return true;
}
let position = get_position(styled_dom, node_id, &styled_node.styled_node_state);
if position.is_absolute_or_fixed() {
return true;
}
let float = get_float(styled_dom, node_id, &styled_node.styled_node_state);
if !float.is_none() {
return true;
}
}
if styled_dom.root.into_crate_internal() == Some(node_id) {
return true;
}
false
}
fn determine_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> FormattingContext {
let node_data = &styled_dom.node_data.as_container()[node_id];
if matches!(node_data.get_node_type(), NodeType::Text(_)) {
return FormattingContext::Inline;
}
let display_type = get_display_type(styled_dom, node_id);
match display_type {
LayoutDisplay::Inline => FormattingContext::Inline,
LayoutDisplay::Block | LayoutDisplay::FlowRoot | LayoutDisplay::ListItem => {
if has_only_inline_children(styled_dom, node_id) {
FormattingContext::Inline
} else {
FormattingContext::Block {
establishes_new_context: establishes_new_block_formatting_context(
styled_dom, node_id,
),
}
}
}
LayoutDisplay::InlineBlock => FormattingContext::InlineBlock,
LayoutDisplay::Table | LayoutDisplay::InlineTable => FormattingContext::Table,
LayoutDisplay::TableRowGroup
| LayoutDisplay::TableHeaderGroup
| LayoutDisplay::TableFooterGroup => FormattingContext::TableRowGroup,
LayoutDisplay::TableRow => FormattingContext::TableRow,
LayoutDisplay::TableCell => FormattingContext::TableCell,
LayoutDisplay::None => FormattingContext::None,
LayoutDisplay::Flex | LayoutDisplay::InlineFlex => FormattingContext::Flex,
LayoutDisplay::TableColumnGroup => FormattingContext::TableColumnGroup,
LayoutDisplay::TableCaption => FormattingContext::TableCaption,
LayoutDisplay::Grid | LayoutDisplay::InlineGrid => FormattingContext::Grid,
LayoutDisplay::TableColumn | LayoutDisplay::RunIn | LayoutDisplay::Marker => {
FormattingContext::Block {
establishes_new_context: true,
}
}
}
}