use std::{
collections::{BTreeMap, HashMap},
sync::Arc,
};
use azul_core::{
dom::{FormattingContext, NodeId, NodeType},
geom::{LogicalPosition, LogicalRect, LogicalSize},
resources::RendererResources,
styled_dom::{StyledDom, StyledNodeState},
};
use azul_css::{
css::CssPropertyValue,
props::{
basic::{
font::{StyleFontStyle, StyleFontWeight},
pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
ColorU, PhysicalSize, PropertyContext, ResolutionContext, SizeMetric,
},
layout::{
ColumnCount, LayoutBorderSpacing, LayoutClear, LayoutDisplay, LayoutFloat,
LayoutHeight, LayoutJustifyContent, LayoutOverflow, LayoutPosition, LayoutTableLayout,
LayoutTextJustify, LayoutWidth, LayoutWritingMode, ShapeInside, ShapeOutside,
StyleBorderCollapse, StyleCaptionSide,
},
property::CssProperty,
style::{
BorderStyle, StyleDirection, StyleHyphens, StyleListStylePosition, StyleListStyleType,
StyleTextAlign, StyleTextCombineUpright, StyleVerticalAlign, StyleVisibility,
StyleWhiteSpace,
},
},
};
use rust_fontconfig::FcWeight;
use taffy::{AvailableSpace, LayoutInput, Line, Size as TaffySize};
#[cfg(feature = "text_layout")]
use crate::text3;
use crate::{
debug_ifc_layout, debug_info, debug_log, debug_table_layout, debug_warning,
font_traits::{
ContentIndex, FontLoaderTrait, ImageSource, InlineContent, InlineImage, InlineShape,
LayoutFragment, ObjectFit, ParsedFontTrait, SegmentAlignment, ShapeBoundary,
ShapeDefinition, ShapedItem, Size, StyleProperties, StyledRun, TextLayoutCache,
UnifiedConstraints,
},
solver3::{
geometry::{BoxProps, EdgeSizes, IntrinsicSizes},
getters::{
get_css_height, get_css_width, get_direction_property,
get_display_property, get_element_font_size, get_float, get_clear,
get_list_style_position, get_list_style_type, get_overflow_x, get_overflow_y,
get_parent_font_size, get_root_font_size, get_style_properties,
get_text_align, get_vertical_align_property, get_visibility,
get_white_space_property, get_writing_mode, MultiValue,
},
layout_tree::{
AnonymousBoxType, CachedInlineLayout, LayoutNode, LayoutTree, PseudoElement,
},
positioning::get_position_type,
scrollbar::ScrollbarRequirements,
sizing::extract_text_from_node,
taffy_bridge, LayoutContext, LayoutDebugMessage, LayoutError, Result,
},
text3::cache::{AvailableSpace as Text3AvailableSpace, TextAlign as Text3TextAlign},
};
pub const DEFAULT_SCROLLBAR_WIDTH_PX: f32 = 16.0;
#[derive(Debug, Clone)]
pub(crate) struct BfcLayoutResult {
pub output: LayoutOutput,
pub escaped_top_margin: Option<f32>,
pub escaped_bottom_margin: Option<f32>,
}
impl BfcLayoutResult {
pub fn from_output(output: LayoutOutput) -> Self {
Self {
output,
escaped_top_margin: None,
escaped_bottom_margin: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OverflowBehavior {
Visible,
Hidden,
Clip,
Scroll,
Auto,
}
impl OverflowBehavior {
pub fn is_clipped(&self) -> bool {
matches!(self, Self::Hidden | Self::Clip | Self::Scroll | Self::Auto)
}
pub fn is_scroll(&self) -> bool {
matches!(self, Self::Scroll | Self::Auto)
}
}
#[derive(Debug)]
pub struct LayoutConstraints<'a> {
pub available_size: LogicalSize,
pub writing_mode: LayoutWritingMode,
pub bfc_state: Option<&'a mut BfcState>,
pub text_align: TextAlign,
pub containing_block_size: LogicalSize,
pub available_width_type: Text3AvailableSpace,
}
#[derive(Debug, Clone)]
pub struct BfcState {
pub pen: LogicalPosition,
pub floats: FloatingContext,
pub margins: MarginCollapseContext,
}
impl BfcState {
pub fn new() -> Self {
Self {
pen: LogicalPosition::zero(),
floats: FloatingContext::default(),
margins: MarginCollapseContext::default(),
}
}
}
#[derive(Debug, Default, Clone)]
pub struct MarginCollapseContext {
pub last_in_flow_margin_bottom: f32,
}
#[derive(Debug, Default, Clone)]
pub struct LayoutOutput {
pub positions: BTreeMap<usize, LogicalPosition>,
pub overflow_size: LogicalSize,
pub baseline: Option<f32>,
}
#[derive(Debug, Clone, Copy, Default)]
pub enum TextAlign {
#[default]
Start,
End,
Center,
Justify,
}
#[derive(Debug, Clone, Copy)]
struct FloatBox {
kind: LayoutFloat,
rect: LogicalRect,
margin: EdgeSizes,
}
#[derive(Debug, Default, Clone)]
pub struct FloatingContext {
pub floats: Vec<FloatBox>,
}
impl FloatingContext {
pub fn add_float(&mut self, kind: LayoutFloat, rect: LogicalRect, margin: EdgeSizes) {
self.floats.push(FloatBox { kind, rect, margin });
}
pub fn available_line_box_space(
&self,
main_start: f32,
main_end: f32,
bfc_cross_size: f32,
wm: LayoutWritingMode,
) -> (f32, f32) {
let mut available_cross_start = 0.0_f32;
let mut available_cross_end = bfc_cross_size;
for float in &self.floats {
let float_main_start = float.rect.origin.main(wm);
let float_main_end = float_main_start + float.rect.size.main(wm);
if main_end > float_main_start && main_start < float_main_end {
let float_cross_start = float.rect.origin.cross(wm);
let float_cross_end = float_cross_start + float.rect.size.cross(wm);
if float.kind == LayoutFloat::Left {
available_cross_start = available_cross_start.max(float_cross_end);
} else {
available_cross_end = available_cross_end.min(float_cross_start);
}
}
}
(available_cross_start, available_cross_end)
}
pub fn clearance_offset(
&self,
clear: LayoutClear,
current_main_offset: f32,
wm: LayoutWritingMode,
) -> f32 {
let mut max_end_offset = 0.0_f32;
let check_left = clear == LayoutClear::Left || clear == LayoutClear::Both;
let check_right = clear == LayoutClear::Right || clear == LayoutClear::Both;
for float in &self.floats {
let should_clear_this_float = (check_left && float.kind == LayoutFloat::Left)
|| (check_right && float.kind == LayoutFloat::Right);
if should_clear_this_float {
let float_margin_box_end = float.rect.origin.main(wm)
+ float.rect.size.main(wm)
+ float.margin.main_end(wm);
max_end_offset = max_end_offset.max(float_margin_box_end);
}
}
if max_end_offset > current_main_offset {
max_end_offset
} else {
current_main_offset
}
}
}
struct BfcLayoutState {
pen: LogicalPosition,
floats: FloatingContext,
margins: MarginCollapseContext,
writing_mode: LayoutWritingMode,
}
#[derive(Debug, Default)]
pub struct LayoutResult {
pub positions: Vec<(usize, LogicalPosition)>,
pub overflow_size: Option<LogicalSize>,
pub baseline_offset: f32,
}
pub fn layout_formatting_context<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut crate::font_traits::TextLayoutCache,
node_index: usize,
constraints: &LayoutConstraints,
float_cache: &mut std::collections::BTreeMap<usize, FloatingContext>,
) -> Result<BfcLayoutResult> {
let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
debug_info!(
ctx,
"[layout_formatting_context] node_index={}, fc={:?}, available_size={:?}",
node_index,
node.formatting_context,
constraints.available_size
);
match node.formatting_context {
FormattingContext::Block { .. } => {
layout_bfc(ctx, tree, text_cache, node_index, constraints, float_cache)
}
FormattingContext::Inline => layout_ifc(ctx, text_cache, tree, node_index, constraints)
.map(BfcLayoutResult::from_output),
FormattingContext::InlineBlock => {
let mut temp_float_cache = std::collections::BTreeMap::new();
layout_bfc(ctx, tree, text_cache, node_index, constraints, &mut temp_float_cache)
}
FormattingContext::Table => layout_table_fc(ctx, tree, text_cache, node_index, constraints)
.map(BfcLayoutResult::from_output),
FormattingContext::Flex | FormattingContext::Grid => {
layout_flex_grid(ctx, tree, text_cache, node_index, constraints)
}
_ => {
let mut temp_float_cache = std::collections::BTreeMap::new();
layout_bfc(
ctx,
tree,
text_cache,
node_index,
constraints,
&mut temp_float_cache,
)
}
}
}
fn layout_flex_grid<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut crate::font_traits::TextLayoutCache,
node_index: usize,
constraints: &LayoutConstraints,
) -> Result<BfcLayoutResult> {
let available_space = TaffySize {
width: AvailableSpace::Definite(constraints.available_size.width),
height: AvailableSpace::Definite(constraints.available_size.height),
};
let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
let (explicit_width, has_explicit_width) =
resolve_explicit_dimension_width(ctx, node, constraints);
let (explicit_height, has_explicit_height) =
resolve_explicit_dimension_height(ctx, node, constraints);
let is_root = node.parent.is_none();
let effective_width = if has_explicit_width {
explicit_width
} else if is_root && constraints.available_size.width.is_finite() {
Some(constraints.available_size.width)
} else {
None
};
let effective_height = if has_explicit_height {
explicit_height
} else if is_root && constraints.available_size.height.is_finite() {
Some(constraints.available_size.height)
} else {
None
};
let has_effective_width = effective_width.is_some();
let has_effective_height = effective_height.is_some();
let width_adjustment = node.box_props.border.left
+ node.box_props.border.right
+ node.box_props.padding.left
+ node.box_props.padding.right;
let height_adjustment = node.box_props.border.top
+ node.box_props.border.bottom
+ node.box_props.padding.top
+ node.box_props.padding.bottom;
let adjusted_width = if has_explicit_width {
explicit_width.map(|w| w + width_adjustment)
} else {
effective_width };
let adjusted_height = if has_explicit_height {
explicit_height.map(|h| h + height_adjustment)
} else {
effective_height };
let sizing_mode = if has_effective_width || has_effective_height {
taffy::SizingMode::InherentSize
} else {
taffy::SizingMode::ContentSize
};
let known_dimensions = TaffySize {
width: adjusted_width,
height: adjusted_height,
};
let parent_size = translate_taffy_size(constraints.containing_block_size);
let taffy_inputs = LayoutInput {
known_dimensions,
parent_size,
available_space,
run_mode: taffy::RunMode::PerformLayout,
sizing_mode,
axis: taffy::RequestedAxis::Both,
vertical_margins_are_collapsible: Line::FALSE,
};
debug_info!(
ctx,
"CALLING LAYOUT_TAFFY FOR FLEX/GRID FC node_index={:?}",
node_index
);
let taffy_output =
taffy_bridge::layout_taffy_subtree(ctx, tree, text_cache, node_index, taffy_inputs);
let mut output = LayoutOutput::default();
output.overflow_size = translate_taffy_size_back(taffy_output.content_size);
let children: Vec<usize> = tree.get(node_index).unwrap().children.clone();
for &child_idx in &children {
if let Some(child_node) = tree.get(child_idx) {
if let Some(pos) = child_node.relative_position {
output.positions.insert(child_idx, pos);
}
}
}
Ok(BfcLayoutResult::from_output(output))
}
fn resolve_explicit_dimension_width<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
node: &LayoutNode,
constraints: &LayoutConstraints,
) -> (Option<f32>, bool) {
node.dom_node_id
.map(|id| {
let width = get_css_width(
ctx.styled_dom,
id,
&ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state,
);
match width.unwrap_or_default() {
LayoutWidth::Auto => (None, false),
LayoutWidth::Px(px) => {
let pixels = resolve_size_metric(
px.metric,
px.number.get(),
constraints.available_size.width,
ctx.viewport_size,
);
(Some(pixels), true)
}
LayoutWidth::MinContent | LayoutWidth::MaxContent => (None, false),
LayoutWidth::Calc(_) => (None, false), }
})
.unwrap_or((None, false))
}
fn resolve_explicit_dimension_height<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
node: &LayoutNode,
constraints: &LayoutConstraints,
) -> (Option<f32>, bool) {
node.dom_node_id
.map(|id| {
let height = get_css_height(
ctx.styled_dom,
id,
&ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state,
);
match height.unwrap_or_default() {
LayoutHeight::Auto => (None, false),
LayoutHeight::Px(px) => {
let pixels = resolve_size_metric(
px.metric,
px.number.get(),
constraints.available_size.height,
ctx.viewport_size,
);
(Some(pixels), true)
}
LayoutHeight::MinContent | LayoutHeight::MaxContent => (None, false),
LayoutHeight::Calc(_) => (None, false), }
})
.unwrap_or((None, false))
}
fn position_float(
float_ctx: &FloatingContext,
float_type: LayoutFloat,
size: LogicalSize,
margin: &EdgeSizes,
current_main_offset: f32,
bfc_cross_size: f32,
wm: LayoutWritingMode,
) -> LogicalRect {
let mut main_start = current_main_offset;
let total_main = size.main(wm) + margin.main_start(wm) + margin.main_end(wm);
let total_cross = size.cross(wm) + margin.cross_start(wm) + margin.cross_end(wm);
let cross_start = loop {
let (avail_start, avail_end) = float_ctx.available_line_box_space(
main_start,
main_start + total_main,
bfc_cross_size,
wm,
);
let available_width = avail_end - avail_start;
if available_width >= total_cross {
if float_type == LayoutFloat::Left {
break avail_start + margin.cross_start(wm);
} else {
break avail_end - total_cross + margin.cross_start(wm);
}
}
let next_main = float_ctx
.floats
.iter()
.filter(|f| {
let f_main_start = f.rect.origin.main(wm);
let f_main_end = f_main_start + f.rect.size.main(wm);
f_main_end > main_start && f_main_start < main_start + total_main
})
.map(|f| f.rect.origin.main(wm) + f.rect.size.main(wm))
.max_by(|a, b| a.partial_cmp(b).unwrap());
if let Some(next) = next_main {
main_start = next;
} else {
if float_type == LayoutFloat::Left {
break avail_start + margin.cross_start(wm);
} else {
break avail_end - total_cross + margin.cross_start(wm);
}
}
};
LogicalRect {
origin: LogicalPosition::from_main_cross(
main_start + margin.main_start(wm),
cross_start,
wm,
),
size,
}
}
fn layout_bfc<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut crate::font_traits::TextLayoutCache,
node_index: usize,
constraints: &LayoutConstraints,
float_cache: &mut std::collections::BTreeMap<usize, FloatingContext>,
) -> Result<BfcLayoutResult> {
let node = tree
.get(node_index)
.ok_or(LayoutError::InvalidTree)?
.clone();
let writing_mode = constraints.writing_mode;
let mut output = LayoutOutput::default();
debug_info!(
ctx,
"\n[layout_bfc] ENTERED for node_index={}, children.len()={}, incoming_bfc_state={}",
node_index,
node.children.len(),
constraints.bfc_state.is_some()
);
let mut float_context = FloatingContext::default();
let mut children_containing_block_size = if let Some(used_size) = node.used_size {
node.box_props.inner_size(used_size, writing_mode)
} else {
constraints.available_size
};
let scrollbar_reservation = node
.dom_node_id
.map(|dom_id| {
let styled_node_state = ctx
.styled_dom
.styled_nodes
.as_container()
.get(dom_id)
.map(|s| s.styled_node_state.clone())
.unwrap_or_default();
let overflow_y =
crate::solver3::getters::get_overflow_y(ctx.styled_dom, dom_id, &styled_node_state);
use azul_css::props::layout::LayoutOverflow;
match overflow_y.unwrap_or_default() {
LayoutOverflow::Scroll | LayoutOverflow::Auto => {
crate::solver3::getters::get_layout_scrollbar_width_px(ctx, dom_id, &styled_node_state)
}
_ => 0.0,
}
})
.unwrap_or(0.0);
if scrollbar_reservation > 0.0 {
children_containing_block_size.width =
(children_containing_block_size.width - scrollbar_reservation).max(0.0);
}
{
let mut temp_positions: super::PositionVec = Vec::new();
let mut temp_scrollbar_reflow = false;
for &child_index in &node.children {
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let child_dom_id = child_node.dom_node_id;
let position_type = get_position_type(ctx.styled_dom, child_dom_id);
if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
continue;
}
crate::solver3::cache::calculate_layout_for_subtree(
ctx,
tree,
text_cache,
child_index,
LogicalPosition::zero(),
children_containing_block_size,
&mut temp_positions,
&mut temp_scrollbar_reflow,
float_cache,
crate::solver3::cache::ComputeMode::ComputeSize,
)?;
}
}
let mut main_pen = 0.0f32;
let mut max_cross_size = 0.0f32;
let mut total_escaped_top_margin = 0.0f32;
let mut total_sibling_margins = 0.0f32;
let mut last_margin_bottom = 0.0f32;
let mut is_first_child = true;
let mut first_child_index: Option<usize> = None;
let mut last_child_index: Option<usize> = None;
let parent_margin_top = node.box_props.margin.main_start(writing_mode);
let parent_margin_bottom = node.box_props.margin.main_end(writing_mode);
let parent_has_top_blocker = has_margin_collapse_blocker(&node.box_props, writing_mode, true);
let parent_has_bottom_blocker =
has_margin_collapse_blocker(&node.box_props, writing_mode, false);
let mut accumulated_top_margin = 0.0f32;
let mut top_margin_resolved = false;
let mut top_margin_escaped = false;
let mut has_content = false;
for &child_index in &node.children {
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let child_dom_id = child_node.dom_node_id;
let position_type = get_position_type(ctx.styled_dom, child_dom_id);
if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
continue;
}
let is_float = if let Some(node_id) = child_dom_id {
let float_type = get_float_property(ctx.styled_dom, Some(node_id));
if float_type != LayoutFloat::None {
let float_size = match child_node.used_size {
Some(size) => size,
None => {
let intrinsic = child_node.intrinsic_sizes.unwrap_or_default();
let computed_size = crate::solver3::sizing::calculate_used_size_for_node(
ctx.styled_dom,
child_dom_id,
children_containing_block_size,
intrinsic,
&child_node.box_props,
ctx.viewport_size,
)?;
if let Some(node_mut) = tree.get_mut(child_index) {
node_mut.used_size = Some(computed_size);
}
computed_size
}
};
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let float_margin = &child_node.box_props.margin;
let float_y = main_pen + last_margin_bottom;
debug_info!(
ctx,
"[layout_bfc] Positioning float: index={}, type={:?}, size={:?}, at Y={} \
(main_pen={} + last_margin={})",
child_index,
float_type,
float_size,
float_y,
main_pen,
last_margin_bottom
);
let float_rect = position_float(
&float_context,
float_type,
float_size,
float_margin,
float_y,
constraints.available_size.cross(writing_mode),
writing_mode,
);
debug_info!(ctx, "[layout_bfc] Float positioned at: {:?}", float_rect);
float_context.add_float(float_type, float_rect, *float_margin);
output.positions.insert(child_index, float_rect.origin);
debug_info!(
ctx,
"[layout_bfc] *** FLOAT POSITIONED: child={}, main_pen={} (unchanged - floats \
don't advance pen)",
child_index,
main_pen
);
continue;
}
false
} else {
false
};
if is_float {
continue;
}
if first_child_index.is_none() {
first_child_index = Some(child_index);
}
last_child_index = Some(child_index);
let child_size = match child_node.used_size {
Some(size) => size,
None => {
let intrinsic = child_node.intrinsic_sizes.unwrap_or_default();
let child_used_size = crate::solver3::sizing::calculate_used_size_for_node(
ctx.styled_dom,
child_dom_id,
children_containing_block_size,
intrinsic,
&child_node.box_props,
ctx.viewport_size,
)?;
if let Some(node_mut) = tree.get_mut(child_index) {
node_mut.used_size = Some(child_used_size);
}
child_used_size
}
};
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let child_margin = &child_node.box_props.margin;
debug_info!(
ctx,
"[layout_bfc] Child {} margin from box_props: top={}, right={}, bottom={}, left={}",
child_index,
child_margin.top,
child_margin.right,
child_margin.bottom,
child_margin.left
);
let child_margin_top = child_margin.main_start(writing_mode);
let child_margin_bottom = child_margin.main_end(writing_mode);
debug_info!(
ctx,
"[layout_bfc] Child {} final margins: margin_top={}, margin_bottom={}",
child_index,
child_margin_top,
child_margin_bottom
);
let child_has_top_blocker =
has_margin_collapse_blocker(&child_node.box_props, writing_mode, true);
let child_has_bottom_blocker =
has_margin_collapse_blocker(&child_node.box_props, writing_mode, false);
let child_clear = if let Some(node_id) = child_dom_id {
get_clear_property(ctx.styled_dom, Some(node_id))
} else {
LayoutClear::None
};
debug_info!(
ctx,
"[layout_bfc] Child {} clear property: {:?}",
child_index,
child_clear
);
let is_empty = is_empty_block(child_node);
if is_empty
&& !child_has_top_blocker
&& !child_has_bottom_blocker
&& child_clear == LayoutClear::None
{
let self_collapsed = collapse_margins(child_margin_top, child_margin_bottom);
if is_first_child {
is_first_child = false;
if !parent_has_top_blocker {
accumulated_top_margin = collapse_margins(parent_margin_top, self_collapsed);
} else {
if accumulated_top_margin == 0.0 {
accumulated_top_margin = parent_margin_top;
}
main_pen += accumulated_top_margin + self_collapsed;
top_margin_resolved = true;
accumulated_top_margin = 0.0;
}
last_margin_bottom = self_collapsed;
} else {
last_margin_bottom = collapse_margins(last_margin_bottom, self_collapsed);
}
continue;
}
let clearance_applied = if child_clear != LayoutClear::None {
let cleared_offset =
float_context.clearance_offset(child_clear, main_pen, writing_mode);
debug_info!(
ctx,
"[layout_bfc] Child {} clearance check: cleared_offset={}, main_pen={}",
child_index,
cleared_offset,
main_pen
);
if cleared_offset > main_pen {
debug_info!(
ctx,
"[layout_bfc] Applying clearance: child={}, clear={:?}, old_pen={}, new_pen={}",
child_index,
child_clear,
main_pen,
cleared_offset
);
main_pen = cleared_offset;
true } else {
false
}
} else {
false
};
if is_first_child {
is_first_child = false;
if clearance_applied {
main_pen += child_margin_top;
debug_info!(
ctx,
"[layout_bfc] First child {} with CLEARANCE: no collapse, child_margin={}, \
main_pen={}",
child_index,
child_margin_top,
main_pen
);
} else if !parent_has_top_blocker {
accumulated_top_margin = collapse_margins(parent_margin_top, child_margin_top);
top_margin_resolved = true;
top_margin_escaped = true;
total_escaped_top_margin = accumulated_top_margin;
debug_info!(
ctx,
"[layout_bfc] First child {} margin ESCAPES: parent_margin={}, \
child_margin={}, collapsed={}, total_escaped={}",
child_index,
parent_margin_top,
child_margin_top,
accumulated_top_margin,
total_escaped_top_margin
);
} else {
main_pen += child_margin_top;
debug_info!(
ctx,
"[layout_bfc] First child {} BLOCKED: parent_has_blocker={}, advanced by \
child_margin={}, main_pen={}",
child_index,
parent_has_top_blocker,
child_margin_top,
main_pen
);
}
} else {
if !top_margin_resolved {
main_pen += accumulated_top_margin;
top_margin_resolved = true;
debug_info!(
ctx,
"[layout_bfc] RESOLVED top margin for node {} at sibling {}: accumulated={}, \
main_pen={}",
node_index,
child_index,
accumulated_top_margin,
main_pen
);
}
if clearance_applied {
main_pen += child_margin_top;
debug_info!(
ctx,
"[layout_bfc] Child {} with CLEARANCE: no collapse with sibling, \
child_margin_top={}, main_pen={}",
child_index,
child_margin_top,
main_pen
);
} else {
let collapsed = collapse_margins(last_margin_bottom, child_margin_top);
main_pen += collapsed;
total_sibling_margins += collapsed;
debug_info!(
ctx,
"[layout_bfc] Sibling collapse for child {}: last_margin_bottom={}, \
child_margin_top={}, collapsed={}, main_pen={}, total_sibling_margins={}",
child_index,
last_margin_bottom,
child_margin_top,
collapsed,
main_pen,
total_sibling_margins
);
}
}
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let establishes_bfc = establishes_new_bfc(ctx, child_node);
let (cross_start, cross_end, available_cross) = if establishes_bfc {
let (start, end) = float_context.available_line_box_space(
main_pen,
main_pen + child_size.main(writing_mode),
constraints.available_size.cross(writing_mode),
writing_mode,
);
let available = end - start;
debug_info!(
ctx,
"[layout_bfc] Child {} establishes BFC: shrinking to avoid floats, \
cross_range={}..{}, available_cross={}",
child_index,
start,
end,
available
);
(start, end, available)
} else {
let start = 0.0;
let end = constraints.available_size.cross(writing_mode);
let available = end - start;
debug_info!(
ctx,
"[layout_bfc] Child {} is normal flow: overlapping floats at full width, \
available_cross={}",
child_index,
available
);
(start, end, available)
};
let (child_margin_cloned, child_margin_auto, child_used_size, is_inline_fc, child_dom_id_for_debug) = {
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
(
child_node.box_props.margin.clone(),
child_node.box_props.margin_auto,
child_node.used_size.unwrap_or_default(),
child_node.formatting_context == FormattingContext::Inline,
child_node.dom_node_id,
)
};
let child_margin = &child_margin_cloned;
debug_info!(
ctx,
"[layout_bfc] Child {} margin_auto: left={}, right={}, top={}, bottom={}",
child_index,
child_margin_auto.left,
child_margin_auto.right,
child_margin_auto.top,
child_margin_auto.bottom
);
debug_info!(
ctx,
"[layout_bfc] Child {} used_size: width={}, height={}",
child_index,
child_used_size.width,
child_used_size.height
);
let (child_cross_pos, mut child_main_pos) = if establishes_bfc {
(
cross_start + child_margin.cross_start(writing_mode),
main_pen,
)
} else {
let available_cross = constraints.available_size.cross(writing_mode);
let child_cross_size = child_used_size.cross(writing_mode);
debug_info!(
ctx,
"[layout_bfc] Child {} centering check: available_cross={}, child_cross_size={}, margin_auto.left={}, margin_auto.right={}",
child_index,
available_cross,
child_cross_size,
child_margin_auto.left,
child_margin_auto.right
);
let cross_pos = if child_margin_auto.left && child_margin_auto.right {
let remaining_space = (available_cross - child_cross_size).max(0.0);
debug_info!(
ctx,
"[layout_bfc] Child {} CENTERING: remaining_space={}, cross_pos={}",
child_index,
remaining_space,
remaining_space / 2.0
);
remaining_space / 2.0
} else if child_margin_auto.left {
let remaining_space = (available_cross - child_cross_size - child_margin.right).max(0.0);
debug_info!(
ctx,
"[layout_bfc] Child {} margin-left:auto only, pushing right: remaining_space={}",
child_index,
remaining_space
);
remaining_space
} else if child_margin_auto.right {
debug_info!(
ctx,
"[layout_bfc] Child {} margin-right:auto only, using left margin={}",
child_index,
child_margin.cross_start(writing_mode)
);
child_margin.cross_start(writing_mode)
} else {
debug_info!(
ctx,
"[layout_bfc] Child {} NO auto margins, using left margin={}",
child_index,
child_margin.cross_start(writing_mode)
);
child_margin.cross_start(writing_mode)
};
(cross_pos, main_pen)
};
let final_pos =
LogicalPosition::from_main_cross(child_main_pos, child_cross_pos, writing_mode);
debug_info!(
ctx,
"[layout_bfc] *** NORMAL FLOW BLOCK POSITIONED: child={}, final_pos={:?}, \
main_pen={}, establishes_bfc={}",
child_index,
final_pos,
main_pen,
establishes_bfc
);
if is_inline_fc && !establishes_bfc {
let floats_for_ifc = float_cache.get(&node_index).unwrap_or(&float_context);
debug_info!(
ctx,
"[layout_bfc] Re-layouting IFC child {} (normal flow) with parent's float context \
at Y={}, child_cross_pos={}",
child_index,
main_pen,
child_cross_pos
);
debug_info!(
ctx,
"[layout_bfc] Using {} floats (from cache: {})",
floats_for_ifc.floats.len(),
float_cache.contains_key(&node_index)
);
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let padding_border_cross = child_node.box_props.padding.cross_start(writing_mode)
+ child_node.box_props.border.cross_start(writing_mode);
let padding_border_main = child_node.box_props.padding.main_start(writing_mode)
+ child_node.box_props.border.main_start(writing_mode);
let content_box_cross = child_cross_pos + padding_border_cross;
let content_box_main = main_pen + padding_border_main;
debug_info!(
ctx,
"[layout_bfc] Border-box at ({}, {}), Content-box at ({}, {}), \
padding+border=({}, {})",
child_cross_pos,
main_pen,
content_box_cross,
content_box_main,
padding_border_cross,
padding_border_main
);
let mut ifc_floats = FloatingContext::default();
for float_box in &floats_for_ifc.floats {
let float_rel_to_ifc = LogicalRect {
origin: LogicalPosition {
x: float_box.rect.origin.x - content_box_cross,
y: float_box.rect.origin.y - content_box_main,
},
size: float_box.rect.size,
};
debug_info!(
ctx,
"[layout_bfc] Float {:?}: BFC coords = {:?}, IFC-content-relative = {:?}",
float_box.kind,
float_box.rect,
float_rel_to_ifc
);
ifc_floats.add_float(float_box.kind, float_rel_to_ifc, float_box.margin);
}
let mut bfc_state = BfcState {
pen: LogicalPosition::zero(), floats: ifc_floats.clone(),
margins: MarginCollapseContext::default(),
};
debug_info!(
ctx,
"[layout_bfc] Created IFC-relative FloatingContext with {} floats",
ifc_floats.floats.len()
);
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let child_dom_id = child_node.dom_node_id;
let display = get_display_property(ctx.styled_dom, child_dom_id).unwrap_or_default();
let child_content_size = if display == LayoutDisplay::Inline {
LogicalSize::new(
children_containing_block_size.width,
children_containing_block_size.height,
)
} else {
child_node.box_props.inner_size(child_size, writing_mode)
};
debug_info!(
ctx,
"[layout_bfc] IFC child size: border-box={:?}, content-box={:?}",
child_size,
child_content_size
);
let ifc_constraints = LayoutConstraints {
available_size: child_content_size,
bfc_state: Some(&mut bfc_state),
writing_mode,
text_align: constraints.text_align,
containing_block_size: constraints.containing_block_size,
available_width_type: Text3AvailableSpace::Definite(child_content_size.width),
};
let ifc_result = layout_formatting_context(
ctx,
tree,
text_cache,
child_index,
&ifc_constraints,
float_cache,
)?;
debug_info!(
ctx,
"[layout_bfc] IFC child {} re-layouted with float context (text will wrap, box \
stays full width)",
child_index
);
}
output.positions.insert(child_index, final_pos);
main_pen += child_size.main(writing_mode);
has_content = true;
if clearance_applied {
last_margin_bottom = 0.0;
} else {
last_margin_bottom = child_margin_bottom;
}
debug_info!(
ctx,
"[layout_bfc] Child {} positioned at final_pos={:?}, size={:?}, advanced main_pen to \
{}, last_margin_bottom={}, clearance_applied={}",
child_index,
final_pos,
child_size,
main_pen,
last_margin_bottom,
clearance_applied
);
let child_cross_extent =
child_cross_pos + child_size.cross(writing_mode) + child_margin.cross_end(writing_mode);
max_cross_size = max_cross_size.max(child_cross_extent);
}
debug_info!(
ctx,
"[layout_bfc] Storing {} floats in cache for node {}",
float_context.floats.len(),
node_index
);
float_cache.insert(node_index, float_context.clone());
let mut escaped_top_margin = None;
let mut escaped_bottom_margin = None;
if top_margin_escaped {
escaped_top_margin = Some(accumulated_top_margin);
debug_info!(
ctx,
"[layout_bfc] Returning escaped top margin: accumulated={}, node={}",
accumulated_top_margin,
node_index
);
} else if !top_margin_resolved && accumulated_top_margin > 0.0 {
escaped_top_margin = Some(accumulated_top_margin);
debug_info!(
ctx,
"[layout_bfc] Escaping top margin (no content): accumulated={}, node={}",
accumulated_top_margin,
node_index
);
} else if !top_margin_resolved {
escaped_top_margin = Some(accumulated_top_margin);
debug_info!(
ctx,
"[layout_bfc] Escaping top margin (zero, no content): accumulated={}, node={}",
accumulated_top_margin,
node_index
);
} else {
debug_info!(
ctx,
"[layout_bfc] NOT escaping top margin: top_margin_resolved={}, escaped={}, \
accumulated={}, node={}",
top_margin_resolved,
top_margin_escaped,
accumulated_top_margin,
node_index
);
}
if let Some(last_idx) = last_child_index {
let last_child = tree.get(last_idx).ok_or(LayoutError::InvalidTree)?;
let last_has_bottom_blocker =
has_margin_collapse_blocker(&last_child.box_props, writing_mode, false);
debug_info!(
ctx,
"[layout_bfc] Bottom margin for node {}: parent_has_bottom_blocker={}, \
last_has_bottom_blocker={}, last_margin_bottom={}, main_pen_before={}",
node_index,
parent_has_bottom_blocker,
last_has_bottom_blocker,
last_margin_bottom,
main_pen
);
if !parent_has_bottom_blocker && !last_has_bottom_blocker && has_content {
let collapsed_bottom = collapse_margins(parent_margin_bottom, last_margin_bottom);
escaped_bottom_margin = Some(collapsed_bottom);
debug_info!(
ctx,
"[layout_bfc] Bottom margin ESCAPED for node {}: collapsed={}",
node_index,
collapsed_bottom
);
} else {
main_pen += last_margin_bottom;
debug_info!(
ctx,
"[layout_bfc] Bottom margin BLOCKED for node {}: added last_margin_bottom={}, \
main_pen_after={}",
node_index,
last_margin_bottom,
main_pen
);
}
} else {
if !top_margin_resolved {
main_pen += parent_margin_top;
}
main_pen += parent_margin_bottom;
}
let is_root_node = node.parent.is_none();
if is_root_node {
if let Some(top) = escaped_top_margin {
for (_, pos) in output.positions.iter_mut() {
let current_main = pos.main(writing_mode);
*pos = LogicalPosition::from_main_cross(
current_main + top,
pos.cross(writing_mode),
writing_mode,
);
}
main_pen += top;
}
if let Some(bottom) = escaped_bottom_margin {
main_pen += bottom;
}
escaped_top_margin = None;
escaped_bottom_margin = None;
}
let content_box_height = main_pen - total_escaped_top_margin;
output.overflow_size =
LogicalSize::from_main_cross(content_box_height, max_cross_size, writing_mode);
debug_info!(
ctx,
"[layout_bfc] FINAL for node {}: main_pen={}, total_escaped_top={}, \
total_sibling_margins={}, content_box_height={}",
node_index,
main_pen,
total_escaped_top_margin,
total_sibling_margins,
content_box_height
);
output.baseline = None;
if let Some(node_mut) = tree.get_mut(node_index) {
node_mut.escaped_top_margin = escaped_top_margin;
node_mut.escaped_bottom_margin = escaped_bottom_margin;
}
if let Some(node_mut) = tree.get_mut(node_index) {
node_mut.baseline = output.baseline;
}
Ok(BfcLayoutResult {
output,
escaped_top_margin,
escaped_bottom_margin,
})
}
fn layout_ifc<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
text_cache: &mut crate::font_traits::TextLayoutCache,
tree: &mut LayoutTree,
node_index: usize,
constraints: &LayoutConstraints,
) -> Result<LayoutOutput> {
let ifc_start = (ctx.get_system_time_fn.cb)();
let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;;
let float_count = constraints
.bfc_state
.as_ref()
.map(|s| s.floats.floats.len())
.unwrap_or(0);
debug_info!(
ctx,
"[layout_ifc] ENTRY: node_index={}, has_bfc_state={}, float_count={}",
node_index,
constraints.bfc_state.is_some(),
float_count
);
debug_ifc_layout!(ctx, "CALLED for node_index={}", node_index);
let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
let ifc_root_dom_id = match node.dom_node_id {
Some(id) => id,
None => {
let parent_dom_id = node
.parent
.and_then(|p| tree.get(p))
.and_then(|n| n.dom_node_id);
if let Some(id) = parent_dom_id {
id
} else {
node.children
.iter()
.filter_map(|&child_idx| tree.get(child_idx))
.filter_map(|n| n.dom_node_id)
.next()
.ok_or(LayoutError::InvalidTree)?
}
}
};
debug_ifc_layout!(ctx, "ifc_root_dom_id={:?}", ifc_root_dom_id);
let phase1_start = (ctx.get_system_time_fn.cb)();
let (inline_content, child_map) =
collect_and_measure_inline_content(ctx, text_cache, tree, node_index, constraints)?;
let _phase1_time = (ctx.get_system_time_fn.cb)().duration_since(&phase1_start);
debug_info!(
ctx,
"[layout_ifc] Collected {} inline content items for node {}",
inline_content.len(),
node_index
);
if inline_content.len() > 10 {
let _text_count = inline_content.iter().filter(|i| matches!(i, InlineContent::Text(_))).count();
let _shape_count = inline_content.iter().filter(|i| matches!(i, InlineContent::Shape(_))).count();
}
for (i, item) in inline_content.iter().enumerate() {
match item {
InlineContent::Text(run) => debug_info!(ctx, " [{}] Text: '{}'", i, run.text),
InlineContent::Marker {
run,
position_outside,
} => debug_info!(
ctx,
" [{}] Marker: '{}' (outside={})",
i,
run.text,
position_outside
),
InlineContent::Shape(_) => debug_info!(ctx, " [{}] Shape", i),
InlineContent::Image(_) => debug_info!(ctx, " [{}] Image", i),
_ => debug_info!(ctx, " [{}] Other", i),
}
}
debug_ifc_layout!(
ctx,
"Collected {} inline content items",
inline_content.len()
);
if inline_content.is_empty() {
debug_warning!(ctx, "inline_content is empty, returning default output!");
return Ok(LayoutOutput::default());
}
let _cached_ifc = tree
.get(node_index)
.and_then(|n| n.inline_layout_result.as_ref());
let text3_constraints =
translate_to_text3_constraints(ctx, constraints, ctx.styled_dom, ifc_root_dom_id);
let cached_constraints = text3_constraints.clone();
debug_info!(
ctx,
"[layout_ifc] CALLING text_cache.layout_flow for node {} with {} exclusions",
node_index,
text3_constraints.shape_exclusions.len()
);
let fragments = vec![LayoutFragment {
id: "main".to_string(),
constraints: text3_constraints,
}];
let phase3_start = (ctx.get_system_time_fn.cb)();
let loaded_fonts = ctx.font_manager.get_loaded_fonts();
let text_layout_result = match text_cache.layout_flow(
&inline_content,
&[],
&fragments,
&ctx.font_manager.font_chain_cache,
&ctx.font_manager.fc_cache,
&loaded_fonts,
ctx.debug_messages,
) {
Ok(result) => result,
Err(e) => {
debug_warning!(ctx, "Text layout failed: {:?}", e);
debug_warning!(
ctx,
"Continuing with zero-sized layout for node {}",
node_index
);
let mut output = LayoutOutput::default();
output.overflow_size = LogicalSize::new(0.0, 0.0);
return Ok(output);
}
};
let _phase3_time = (ctx.get_system_time_fn.cb)().duration_since(&phase3_start);
let _total_ifc_time = (ctx.get_system_time_fn.cb)().duration_since(&ifc_start);
let mut output = LayoutOutput::default();
let node = tree.get_mut(node_index).ok_or(LayoutError::InvalidTree)?;
debug_ifc_layout!(
ctx,
"text_layout_result has {} fragment_layouts",
text_layout_result.fragment_layouts.len()
);
if let Some(main_frag) = text_layout_result.fragment_layouts.get("main") {
let frag_bounds = main_frag.bounds();
debug_ifc_layout!(
ctx,
"Found 'main' fragment with {} items, bounds={}x{}",
main_frag.items.len(),
frag_bounds.width,
frag_bounds.height
);
debug_ifc_layout!(ctx, "Storing inline_layout_result on node {}", node_index);
let has_floats = constraints
.bfc_state
.as_ref()
.map(|s| !s.floats.floats.is_empty())
.unwrap_or(false);
let current_width_type = constraints.available_width_type;
let should_store = match &node.inline_layout_result {
None => {
debug_info!(
ctx,
"[layout_ifc] Storing NEW inline_layout_result for node {} (width_type={:?}, \
has_floats={})",
node_index,
current_width_type,
has_floats
);
true
}
Some(cached) => {
if cached.should_replace_with(current_width_type, has_floats) {
debug_info!(
ctx,
"[layout_ifc] REPLACING inline_layout_result for node {} (old: \
width={:?}, floats={}) with (new: width={:?}, floats={})",
node_index,
cached.available_width,
cached.has_floats,
current_width_type,
has_floats
);
true
} else {
debug_info!(
ctx,
"[layout_ifc] KEEPING cached inline_layout_result for node {} (cached: \
width={:?}, floats={}, new: width={:?}, floats={})",
node_index,
cached.available_width,
cached.has_floats,
current_width_type,
has_floats
);
false
}
}
};
if should_store {
node.inline_layout_result = Some(CachedInlineLayout::new_with_constraints(
main_frag.clone(),
current_width_type,
has_floats,
cached_constraints,
));
}
output.overflow_size = LogicalSize::new(frag_bounds.width, frag_bounds.height);
output.baseline = main_frag.last_baseline();
node.baseline = output.baseline;
for positioned_item in &main_frag.items {
if let ShapedItem::Object { source, content, .. } = &positioned_item.item {
if let Some(&child_node_index) = child_map.get(source) {
let new_relative_pos = LogicalPosition {
x: positioned_item.position.x,
y: positioned_item.position.y,
};
output.positions.insert(child_node_index, new_relative_pos);
}
}
}
}
Ok(output)
}
fn translate_taffy_size(size: LogicalSize) -> TaffySize<Option<f32>> {
TaffySize {
width: Some(size.width),
height: Some(size.height),
}
}
pub(crate) fn convert_font_style(style: StyleFontStyle) -> crate::font_traits::FontStyle {
match style {
StyleFontStyle::Normal => crate::font_traits::FontStyle::Normal,
StyleFontStyle::Italic => crate::font_traits::FontStyle::Italic,
StyleFontStyle::Oblique => crate::font_traits::FontStyle::Oblique,
}
}
pub(crate) fn convert_font_weight(weight: StyleFontWeight) -> FcWeight {
match weight {
StyleFontWeight::W100 => FcWeight::Thin,
StyleFontWeight::W200 => FcWeight::ExtraLight,
StyleFontWeight::W300 | StyleFontWeight::Lighter => FcWeight::Light,
StyleFontWeight::Normal => FcWeight::Normal,
StyleFontWeight::W500 => FcWeight::Medium,
StyleFontWeight::W600 => FcWeight::SemiBold,
StyleFontWeight::Bold => FcWeight::Bold,
StyleFontWeight::W800 => FcWeight::ExtraBold,
StyleFontWeight::W900 | StyleFontWeight::Bolder => FcWeight::Black,
}
}
#[inline]
fn resolve_size_metric(
metric: SizeMetric,
value: f32,
containing_block_size: f32,
viewport_size: LogicalSize,
) -> f32 {
match metric {
SizeMetric::Px => value,
SizeMetric::Pt => value * PT_TO_PX,
SizeMetric::Percent => value / 100.0 * containing_block_size,
SizeMetric::Em | SizeMetric::Rem => value * DEFAULT_FONT_SIZE,
SizeMetric::Vw => value / 100.0 * viewport_size.width,
SizeMetric::Vh => value / 100.0 * viewport_size.height,
SizeMetric::Vmin => value / 100.0 * viewport_size.width.min(viewport_size.height),
SizeMetric::Vmax => value / 100.0 * viewport_size.width.max(viewport_size.height),
SizeMetric::In => value * 96.0,
SizeMetric::Cm => value * 96.0 / 2.54,
SizeMetric::Mm => value * 96.0 / 25.4,
}
}
pub fn translate_taffy_size_back(size: TaffySize<f32>) -> LogicalSize {
LogicalSize {
width: size.width,
height: size.height,
}
}
pub fn translate_taffy_point_back(point: taffy::Point<f32>) -> LogicalPosition {
LogicalPosition {
x: point.x,
y: point.y,
}
}
fn establishes_new_bfc<T: ParsedFontTrait>(ctx: &LayoutContext<'_, T>, node: &LayoutNode) -> bool {
let Some(dom_id) = node.dom_node_id else {
return false;
};
let node_state = &ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
let float_val = get_float(ctx.styled_dom, dom_id, node_state);
if matches!(
float_val,
MultiValue::Exact(LayoutFloat::Left | LayoutFloat::Right)
) {
return true;
}
let position = crate::solver3::positioning::get_position_type(ctx.styled_dom, Some(dom_id));
if matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed) {
return true;
}
let display = get_display_property(ctx.styled_dom, Some(dom_id));
if matches!(
display,
MultiValue::Exact(
LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::TableCaption
)
) {
return true;
}
if matches!(display, MultiValue::Exact(LayoutDisplay::FlowRoot)) {
return true;
}
let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, node_state);
let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, node_state);
let creates_bfc_via_overflow = |ov: &MultiValue<LayoutOverflow>| {
matches!(
ov,
&MultiValue::Exact(
LayoutOverflow::Hidden | LayoutOverflow::Scroll | LayoutOverflow::Auto
)
)
};
if creates_bfc_via_overflow(&overflow_x) || creates_bfc_via_overflow(&overflow_y) {
return true;
}
if matches!(
node.formatting_context,
FormattingContext::Table | FormattingContext::Flex | FormattingContext::Grid
) {
return true;
}
false
}
fn translate_to_text3_constraints<'a, T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
constraints: &'a LayoutConstraints<'a>,
styled_dom: &StyledDom,
dom_id: NodeId,
) -> UnifiedConstraints {
let mut shape_exclusions = if let Some(ref bfc_state) = constraints.bfc_state {
debug_info!(
ctx,
"[translate_to_text3] dom_id={:?}, converting {} floats to exclusions",
dom_id,
bfc_state.floats.floats.len()
);
bfc_state
.floats
.floats
.iter()
.enumerate()
.map(|(i, float_box)| {
let rect = crate::text3::cache::Rect {
x: float_box.rect.origin.x,
y: float_box.rect.origin.y,
width: float_box.rect.size.width,
height: float_box.rect.size.height,
};
debug_info!(
ctx,
"[translate_to_text3] Exclusion #{}: {:?} at ({}, {}) size {}x{}",
i,
float_box.kind,
rect.x,
rect.y,
rect.width,
rect.height
);
ShapeBoundary::Rectangle(rect)
})
.collect()
} else {
debug_info!(
ctx,
"[translate_to_text3] dom_id={:?}, NO bfc_state - no float exclusions",
dom_id
);
Vec::new()
};
debug_info!(
ctx,
"[translate_to_text3] dom_id={:?}, available_size={}x{}, shape_exclusions.len()={}",
dom_id,
constraints.available_size.width,
constraints.available_size.height,
shape_exclusions.len()
);
let id = dom_id;
let node_data = &styled_dom.node_data.as_container()[id];
let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
let ref_box_height = if constraints.available_size.height.is_finite() {
constraints.available_size.height
} else {
styled_dom
.css_property_cache
.ptr
.get_height(node_data, &id, node_state)
.and_then(|v| v.get_property())
.and_then(|h| match h {
LayoutHeight::Px(v) => {
match v.metric {
SizeMetric::Px => Some(v.number.get()),
SizeMetric::Pt => Some(v.number.get() * PT_TO_PX),
SizeMetric::In => Some(v.number.get() * 96.0),
SizeMetric::Cm => Some(v.number.get() * 96.0 / 2.54),
SizeMetric::Mm => Some(v.number.get() * 96.0 / 25.4),
_ => None, }
}
_ => None,
})
.unwrap_or(constraints.available_size.width) };
let reference_box = crate::text3::cache::Rect {
x: 0.0,
y: 0.0,
width: constraints.available_size.width,
height: ref_box_height,
};
debug_info!(ctx, "Checking shape-inside for node {:?}", id);
debug_info!(
ctx,
"Reference box: {:?} (available_size height was: {})",
reference_box,
constraints.available_size.height
);
let shape_boundaries = styled_dom
.css_property_cache
.ptr
.get_shape_inside(node_data, &id, node_state)
.and_then(|v| {
debug_info!(ctx, "Got shape-inside value: {:?}", v);
v.get_property()
})
.and_then(|shape_inside| {
debug_info!(ctx, "shape-inside property: {:?}", shape_inside);
if let ShapeInside::Shape(css_shape) = shape_inside {
debug_info!(
ctx,
"Converting CSS shape to ShapeBoundary: {:?}",
css_shape
);
let boundary =
ShapeBoundary::from_css_shape(css_shape, reference_box, ctx.debug_messages);
debug_info!(ctx, "Created ShapeBoundary: {:?}", boundary);
Some(vec![boundary])
} else {
debug_info!(ctx, "shape-inside is None");
None
}
})
.unwrap_or_default();
debug_info!(
ctx,
"Final shape_boundaries count: {}",
shape_boundaries.len()
);
debug_info!(ctx, "Checking shape-outside for node {:?}", id);
if let Some(shape_outside_value) = styled_dom
.css_property_cache
.ptr
.get_shape_outside(node_data, &id, node_state)
{
debug_info!(ctx, "Got shape-outside value: {:?}", shape_outside_value);
if let Some(shape_outside) = shape_outside_value.get_property() {
debug_info!(ctx, "shape-outside property: {:?}", shape_outside);
if let ShapeOutside::Shape(css_shape) = shape_outside {
debug_info!(
ctx,
"Converting CSS shape-outside to ShapeBoundary: {:?}",
css_shape
);
let boundary =
ShapeBoundary::from_css_shape(css_shape, reference_box, ctx.debug_messages);
debug_info!(ctx, "Created ShapeBoundary (exclusion): {:?}", boundary);
shape_exclusions.push(boundary);
}
}
} else {
debug_info!(ctx, "No shape-outside value found");
}
let writing_mode = get_writing_mode(styled_dom, id, node_state).unwrap_or_default();
let text_align = get_text_align(styled_dom, id, node_state).unwrap_or_default();
let text_justify = styled_dom
.css_property_cache
.ptr
.get_text_justify(node_data, &id, node_state)
.and_then(|s| s.get_property().copied())
.unwrap_or_default();
let font_size = get_element_font_size(styled_dom, id, node_state);
let line_height_value = styled_dom
.css_property_cache
.ptr
.get_line_height(node_data, &id, node_state)
.and_then(|s| s.get_property().cloned())
.unwrap_or_default();
let hyphenation = styled_dom
.css_property_cache
.ptr
.get_hyphens(node_data, &id, node_state)
.and_then(|s| s.get_property().copied())
.unwrap_or_default();
let overflow_behaviour = get_overflow_x(styled_dom, id, node_state).unwrap_or_default();
let vertical_align = match get_vertical_align_property(styled_dom, id, node_state) {
MultiValue::Exact(v) => v,
_ => StyleVerticalAlign::default(),
};
let vertical_align = match vertical_align {
StyleVerticalAlign::Baseline => text3::cache::VerticalAlign::Baseline,
StyleVerticalAlign::Top => text3::cache::VerticalAlign::Top,
StyleVerticalAlign::Middle => text3::cache::VerticalAlign::Middle,
StyleVerticalAlign::Bottom => text3::cache::VerticalAlign::Bottom,
StyleVerticalAlign::Sub => text3::cache::VerticalAlign::Sub,
StyleVerticalAlign::Superscript => text3::cache::VerticalAlign::Super,
StyleVerticalAlign::TextTop => text3::cache::VerticalAlign::TextTop,
StyleVerticalAlign::TextBottom => text3::cache::VerticalAlign::TextBottom,
};
let text_orientation = text3::cache::TextOrientation::default();
let direction = match get_direction_property(styled_dom, id, node_state) {
MultiValue::Exact(d) => Some(match d {
StyleDirection::Ltr => text3::cache::BidiDirection::Ltr,
StyleDirection::Rtl => text3::cache::BidiDirection::Rtl,
}),
_ => None,
};
debug_info!(
ctx,
"dom_id={:?}, available_size={}x{}, setting available_width={}",
dom_id,
constraints.available_size.width,
constraints.available_size.height,
constraints.available_size.width
);
let text_indent = styled_dom
.css_property_cache
.ptr
.get_text_indent(node_data, &id, node_state)
.and_then(|s| s.get_property())
.map(|ti| {
let context = ResolutionContext {
element_font_size: get_element_font_size(styled_dom, id, node_state),
parent_font_size: get_parent_font_size(styled_dom, id, node_state),
root_font_size: get_root_font_size(styled_dom, node_state),
containing_block_size: PhysicalSize::new(constraints.available_size.width, 0.0),
element_size: None,
viewport_size: PhysicalSize::new(0.0, 0.0),
};
ti.inner
.resolve_with_context(&context, PropertyContext::Other)
})
.unwrap_or(0.0);
let columns = styled_dom
.css_property_cache
.ptr
.get_column_count(node_data, &id, node_state)
.and_then(|s| s.get_property())
.map(|cc| match cc {
ColumnCount::Integer(n) => *n,
ColumnCount::Auto => 1,
})
.unwrap_or(1);
let column_gap = styled_dom
.css_property_cache
.ptr
.get_column_gap(node_data, &id, node_state)
.and_then(|s| s.get_property())
.map(|cg| {
let context = ResolutionContext {
element_font_size: get_element_font_size(styled_dom, id, node_state),
parent_font_size: get_parent_font_size(styled_dom, id, node_state),
root_font_size: get_root_font_size(styled_dom, node_state),
containing_block_size: PhysicalSize::new(0.0, 0.0),
element_size: None,
viewport_size: PhysicalSize::new(0.0, 0.0),
};
cg.inner
.resolve_with_context(&context, PropertyContext::Other)
})
.unwrap_or_else(|| {
get_element_font_size(styled_dom, id, node_state)
});
let text_wrap = match get_white_space_property(styled_dom, id, node_state) {
MultiValue::Exact(ws) => match ws {
StyleWhiteSpace::Normal => text3::cache::TextWrap::Wrap,
StyleWhiteSpace::Nowrap => text3::cache::TextWrap::NoWrap,
StyleWhiteSpace::Pre => text3::cache::TextWrap::NoWrap,
StyleWhiteSpace::PreWrap => text3::cache::TextWrap::Wrap,
StyleWhiteSpace::PreLine => text3::cache::TextWrap::Wrap,
StyleWhiteSpace::BreakSpaces => text3::cache::TextWrap::Wrap,
},
_ => text3::cache::TextWrap::Wrap,
};
let initial_letter = styled_dom
.css_property_cache
.ptr
.get_initial_letter(node_data, &id, node_state)
.and_then(|s| s.get_property())
.map(|il| {
use std::num::NonZeroUsize;
let sink = match il.sink {
azul_css::corety::OptionU32::Some(s) => s,
azul_css::corety::OptionU32::None => il.size,
};
text3::cache::InitialLetter {
size: il.size as f32,
sink,
count: NonZeroUsize::new(1).unwrap(),
}
});
let line_clamp = styled_dom
.css_property_cache
.ptr
.get_line_clamp(node_data, &id, node_state)
.and_then(|s| s.get_property())
.and_then(|lc| std::num::NonZeroUsize::new(lc.max_lines));
let hanging_punctuation = styled_dom
.css_property_cache
.ptr
.get_hanging_punctuation(node_data, &id, node_state)
.and_then(|s| s.get_property())
.map(|hp| hp.enabled)
.unwrap_or(false);
let text_combine_upright = styled_dom
.css_property_cache
.ptr
.get_text_combine_upright(node_data, &id, node_state)
.and_then(|s| s.get_property())
.map(|tcu| match tcu {
StyleTextCombineUpright::None => text3::cache::TextCombineUpright::None,
StyleTextCombineUpright::All => text3::cache::TextCombineUpright::All,
StyleTextCombineUpright::Digits(n) => text3::cache::TextCombineUpright::Digits(*n),
});
let exclusion_margin = styled_dom
.css_property_cache
.ptr
.get_exclusion_margin(node_data, &id, node_state)
.and_then(|s| s.get_property())
.map(|em| em.inner.get() as f32)
.unwrap_or(0.0);
let hyphenation_language = styled_dom
.css_property_cache
.ptr
.get_hyphenation_language(node_data, &id, node_state)
.and_then(|s| s.get_property())
.and_then(|hl| {
#[cfg(feature = "text_layout_hyphenation")]
{
use hyphenation::{Language, Load};
match hl.inner.as_str() {
"en-US" | "en" => Some(Language::EnglishUS),
"de-DE" | "de" => Some(Language::German1996),
"fr-FR" | "fr" => Some(Language::French),
"es-ES" | "es" => Some(Language::Spanish),
"it-IT" | "it" => Some(Language::Italian),
"pt-PT" | "pt" => Some(Language::Portuguese),
"nl-NL" | "nl" => Some(Language::Dutch),
"pl-PL" | "pl" => Some(Language::Polish),
"ru-RU" | "ru" => Some(Language::Russian),
"zh-CN" | "zh" => Some(Language::Chinese),
_ => None, }
}
#[cfg(not(feature = "text_layout_hyphenation"))]
{
None::<crate::text3::script::Language>
}
});
UnifiedConstraints {
exclusion_margin,
hyphenation_language,
text_indent,
initial_letter,
line_clamp,
columns,
column_gap,
hanging_punctuation,
text_wrap,
text_combine_upright,
segment_alignment: SegmentAlignment::Total,
overflow: match overflow_behaviour {
LayoutOverflow::Visible => text3::cache::OverflowBehavior::Visible,
LayoutOverflow::Hidden | LayoutOverflow::Clip => text3::cache::OverflowBehavior::Hidden,
LayoutOverflow::Scroll => text3::cache::OverflowBehavior::Scroll,
LayoutOverflow::Auto => text3::cache::OverflowBehavior::Auto,
},
available_width: constraints.available_width_type,
available_height: match overflow_behaviour {
LayoutOverflow::Scroll | LayoutOverflow::Auto => None,
_ => Some(constraints.available_size.height),
},
shape_boundaries, shape_exclusions, writing_mode: Some(match writing_mode {
LayoutWritingMode::HorizontalTb => text3::cache::WritingMode::HorizontalTb,
LayoutWritingMode::VerticalRl => text3::cache::WritingMode::VerticalRl,
LayoutWritingMode::VerticalLr => text3::cache::WritingMode::VerticalLr,
}),
direction, hyphenation: match hyphenation {
StyleHyphens::None => false,
StyleHyphens::Auto => true,
},
text_orientation,
text_align: match text_align {
StyleTextAlign::Start => text3::cache::TextAlign::Start,
StyleTextAlign::End => text3::cache::TextAlign::End,
StyleTextAlign::Left => text3::cache::TextAlign::Left,
StyleTextAlign::Right => text3::cache::TextAlign::Right,
StyleTextAlign::Center => text3::cache::TextAlign::Center,
StyleTextAlign::Justify => text3::cache::TextAlign::Justify,
},
text_justify: match text_justify {
LayoutTextJustify::None => text3::cache::JustifyContent::None,
LayoutTextJustify::Auto => text3::cache::JustifyContent::None,
LayoutTextJustify::InterWord => text3::cache::JustifyContent::InterWord,
LayoutTextJustify::InterCharacter => text3::cache::JustifyContent::InterCharacter,
LayoutTextJustify::Distribute => text3::cache::JustifyContent::Distribute,
},
line_height: line_height_value.inner.normalized() * font_size,
vertical_align, }
}
#[derive(Debug, Clone)]
pub struct TableColumnInfo {
pub min_width: f32,
pub max_width: f32,
pub computed_width: Option<f32>,
}
#[derive(Debug, Clone)]
pub struct TableCellInfo {
pub node_index: usize,
pub column: usize,
pub colspan: usize,
pub row: usize,
pub rowspan: usize,
}
#[derive(Debug)]
struct TableLayoutContext {
columns: Vec<TableColumnInfo>,
cells: Vec<TableCellInfo>,
num_rows: usize,
use_fixed_layout: bool,
row_heights: Vec<f32>,
border_collapse: StyleBorderCollapse,
border_spacing: LayoutBorderSpacing,
caption_index: Option<usize>,
collapsed_rows: std::collections::HashSet<usize>,
collapsed_columns: std::collections::HashSet<usize>,
}
impl TableLayoutContext {
fn new() -> Self {
Self {
columns: Vec::new(),
cells: Vec::new(),
num_rows: 0,
use_fixed_layout: false,
row_heights: Vec::new(),
border_collapse: StyleBorderCollapse::Separate,
border_spacing: LayoutBorderSpacing::default(),
caption_index: None,
collapsed_rows: std::collections::HashSet::new(),
collapsed_columns: std::collections::HashSet::new(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum BorderSource {
Table = 0,
ColumnGroup = 1,
Column = 2,
RowGroup = 3,
Row = 4,
Cell = 5,
}
#[derive(Debug, Clone)]
pub struct BorderInfo {
pub width: f32,
pub style: BorderStyle,
pub color: ColorU,
pub source: BorderSource,
}
impl BorderInfo {
pub fn new(width: f32, style: BorderStyle, color: ColorU, source: BorderSource) -> Self {
Self {
width,
style,
color,
source,
}
}
pub fn style_priority(style: &BorderStyle) -> u8 {
match style {
BorderStyle::Hidden => 255, BorderStyle::None => 0, BorderStyle::Double => 8,
BorderStyle::Solid => 7,
BorderStyle::Dashed => 6,
BorderStyle::Dotted => 5,
BorderStyle::Ridge => 4,
BorderStyle::Outset => 3,
BorderStyle::Groove => 2,
BorderStyle::Inset => 1,
}
}
pub fn resolve_conflict(a: &BorderInfo, b: &BorderInfo) -> Option<BorderInfo> {
if a.style == BorderStyle::Hidden || b.style == BorderStyle::Hidden {
return None;
}
let a_is_none = a.style == BorderStyle::None;
let b_is_none = b.style == BorderStyle::None;
if a_is_none && b_is_none {
return None;
}
if a_is_none {
return Some(b.clone());
}
if b_is_none {
return Some(a.clone());
}
if a.width > b.width {
return Some(a.clone());
}
if b.width > a.width {
return Some(b.clone());
}
let a_priority = Self::style_priority(&a.style);
let b_priority = Self::style_priority(&b.style);
if a_priority > b_priority {
return Some(a.clone());
}
if b_priority > a_priority {
return Some(b.clone());
}
if a.source > b.source {
return Some(a.clone());
}
if b.source > a.source {
return Some(b.clone());
}
Some(a.clone())
}
}
fn get_border_info<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
node: &LayoutNode,
source: BorderSource,
) -> (BorderInfo, BorderInfo, BorderInfo, BorderInfo) {
use azul_css::props::{
basic::{
pixel::{PhysicalSize, PropertyContext, ResolutionContext},
ColorU,
},
style::BorderStyle,
};
use get_element_font_size;
use get_parent_font_size;
use get_root_font_size;
let default_border = BorderInfo::new(
0.0,
BorderStyle::None,
ColorU {
r: 0,
g: 0,
b: 0,
a: 0,
},
source,
);
let Some(dom_id) = node.dom_node_id else {
return (
default_border.clone(),
default_border.clone(),
default_border.clone(),
default_border.clone(),
);
};
let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
let node_state = StyledNodeState::default();
let cache = &ctx.styled_dom.css_property_cache.ptr;
if let Some(ref cc) = cache.compact_cache {
let idx = dom_id.index();
let bts = cc.get_border_top_style(idx);
let brs = cc.get_border_right_style(idx);
let bbs = cc.get_border_bottom_style(idx);
let bls = cc.get_border_left_style(idx);
let make_color = |raw: u32| -> ColorU {
if raw == 0 {
ColorU { r: 0, g: 0, b: 0, a: 0 }
} else {
ColorU {
r: ((raw >> 24) & 0xFF) as u8,
g: ((raw >> 16) & 0xFF) as u8,
b: ((raw >> 8) & 0xFF) as u8,
a: (raw & 0xFF) as u8,
}
}
};
let btc = make_color(cc.get_border_top_color_raw(idx));
let brc = make_color(cc.get_border_right_color_raw(idx));
let bbc = make_color(cc.get_border_bottom_color_raw(idx));
let blc = make_color(cc.get_border_left_color_raw(idx));
let decode_width = |raw: i16| -> f32 {
if raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
0.0 } else {
raw as f32 / 10.0
}
};
let btw = decode_width(cc.get_border_top_width_raw(idx));
let brw = decode_width(cc.get_border_right_width_raw(idx));
let bbw = decode_width(cc.get_border_bottom_width_raw(idx));
let blw = decode_width(cc.get_border_left_width_raw(idx));
let top = if bts == BorderStyle::None { default_border.clone() }
else { BorderInfo::new(btw, bts, btc, source) };
let right = if brs == BorderStyle::None { default_border.clone() }
else { BorderInfo::new(brw, brs, brc, source) };
let bottom = if bbs == BorderStyle::None { default_border.clone() }
else { BorderInfo::new(bbw, bbs, bbc, source) };
let left = if bls == BorderStyle::None { default_border.clone() }
else { BorderInfo::new(blw, bls, blc, source) };
return (top, right, bottom, left);
}
let cache = &ctx.styled_dom.css_property_cache.ptr;
let element_font_size = get_element_font_size(ctx.styled_dom, dom_id, &node_state);
let parent_font_size = get_parent_font_size(ctx.styled_dom, dom_id, &node_state);
let root_font_size = get_root_font_size(ctx.styled_dom, &node_state);
let resolution_context = ResolutionContext {
element_font_size,
parent_font_size,
root_font_size,
containing_block_size: PhysicalSize::new(0.0, 0.0),
element_size: None,
viewport_size: PhysicalSize::new(0.0, 0.0),
};
let top = cache
.get_border_top_style(node_data, &dom_id, &node_state)
.and_then(|s| s.get_property())
.map(|style_val| {
let width = cache
.get_border_top_width(node_data, &dom_id, &node_state)
.and_then(|w| w.get_property())
.map(|w| {
w.inner
.resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
})
.unwrap_or(0.0);
let color = cache
.get_border_top_color(node_data, &dom_id, &node_state)
.and_then(|c| c.get_property())
.map(|c| c.inner)
.unwrap_or(ColorU {
r: 0,
g: 0,
b: 0,
a: 255,
});
BorderInfo::new(width, style_val.inner, color, source)
})
.unwrap_or_else(|| default_border.clone());
let right = cache
.get_border_right_style(node_data, &dom_id, &node_state)
.and_then(|s| s.get_property())
.map(|style_val| {
let width = cache
.get_border_right_width(node_data, &dom_id, &node_state)
.and_then(|w| w.get_property())
.map(|w| {
w.inner
.resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
})
.unwrap_or(0.0);
let color = cache
.get_border_right_color(node_data, &dom_id, &node_state)
.and_then(|c| c.get_property())
.map(|c| c.inner)
.unwrap_or(ColorU {
r: 0,
g: 0,
b: 0,
a: 255,
});
BorderInfo::new(width, style_val.inner, color, source)
})
.unwrap_or_else(|| default_border.clone());
let bottom = cache
.get_border_bottom_style(node_data, &dom_id, &node_state)
.and_then(|s| s.get_property())
.map(|style_val| {
let width = cache
.get_border_bottom_width(node_data, &dom_id, &node_state)
.and_then(|w| w.get_property())
.map(|w| {
w.inner
.resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
})
.unwrap_or(0.0);
let color = cache
.get_border_bottom_color(node_data, &dom_id, &node_state)
.and_then(|c| c.get_property())
.map(|c| c.inner)
.unwrap_or(ColorU {
r: 0,
g: 0,
b: 0,
a: 255,
});
BorderInfo::new(width, style_val.inner, color, source)
})
.unwrap_or_else(|| default_border.clone());
let left = cache
.get_border_left_style(node_data, &dom_id, &node_state)
.and_then(|s| s.get_property())
.map(|style_val| {
let width = cache
.get_border_left_width(node_data, &dom_id, &node_state)
.and_then(|w| w.get_property())
.map(|w| {
w.inner
.resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
})
.unwrap_or(0.0);
let color = cache
.get_border_left_color(node_data, &dom_id, &node_state)
.and_then(|c| c.get_property())
.map(|c| c.inner)
.unwrap_or(ColorU {
r: 0,
g: 0,
b: 0,
a: 255,
});
BorderInfo::new(width, style_val.inner, color, source)
})
.unwrap_or_else(|| default_border.clone());
(top, right, bottom, left)
}
fn get_table_layout_property<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
node: &LayoutNode,
) -> LayoutTableLayout {
let Some(dom_id) = node.dom_node_id else {
return LayoutTableLayout::Auto;
};
let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
let node_state = StyledNodeState::default();
ctx.styled_dom
.css_property_cache
.ptr
.get_table_layout(node_data, &dom_id, &node_state)
.and_then(|prop| prop.get_property().copied())
.unwrap_or(LayoutTableLayout::Auto)
}
fn get_border_collapse_property<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
node: &LayoutNode,
) -> StyleBorderCollapse {
let Some(dom_id) = node.dom_node_id else {
return StyleBorderCollapse::Separate;
};
if let Some(ref cc) = ctx.styled_dom.css_property_cache.ptr.compact_cache {
return cc.get_border_collapse(dom_id.index());
}
let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
let node_state = StyledNodeState::default();
ctx.styled_dom
.css_property_cache
.ptr
.get_border_collapse(node_data, &dom_id, &node_state)
.and_then(|prop| prop.get_property().copied())
.unwrap_or(StyleBorderCollapse::Separate)
}
fn get_border_spacing_property<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
node: &LayoutNode,
) -> LayoutBorderSpacing {
if let Some(dom_id) = node.dom_node_id {
if let Some(ref cc) = ctx.styled_dom.css_property_cache.ptr.compact_cache {
let idx = dom_id.index();
let h_raw = cc.get_border_spacing_h_raw(idx);
let v_raw = cc.get_border_spacing_v_raw(idx);
if h_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
&& v_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
{
return LayoutBorderSpacing::new_separate(
azul_css::props::basic::pixel::PixelValue::px(h_raw as f32 / 10.0),
azul_css::props::basic::pixel::PixelValue::px(v_raw as f32 / 10.0),
);
}
}
let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
let node_state = StyledNodeState::default();
if let Some(prop) = ctx.styled_dom.css_property_cache.ptr.get_border_spacing(
node_data,
&dom_id,
&node_state,
) {
if let Some(value) = prop.get_property() {
return *value;
}
}
}
LayoutBorderSpacing::default() }
fn get_caption_side_property<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
node: &LayoutNode,
) -> StyleCaptionSide {
if let Some(dom_id) = node.dom_node_id {
let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
let node_state = StyledNodeState::default();
if let Some(prop) =
ctx.styled_dom
.css_property_cache
.ptr
.get_caption_side(node_data, &dom_id, &node_state)
{
if let Some(value) = prop.get_property() {
return *value;
}
}
}
StyleCaptionSide::Top }
fn is_visibility_collapsed<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
node: &LayoutNode,
) -> bool {
if let Some(dom_id) = node.dom_node_id {
let node_state = StyledNodeState::default();
if let MultiValue::Exact(value) = get_visibility(ctx.styled_dom, dom_id, &node_state) {
return matches!(value, StyleVisibility::Collapse);
}
}
false
}
fn is_cell_empty(tree: &LayoutTree, cell_index: usize) -> bool {
let cell_node = match tree.get(cell_index) {
Some(node) => node,
None => return true, };
if cell_node.children.is_empty() {
return true;
}
if let Some(ref cached_layout) = cell_node.inline_layout_result {
return cached_layout.layout.items.is_empty();
}
false
}
pub fn layout_table_fc<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut crate::font_traits::TextLayoutCache,
node_index: usize,
constraints: &LayoutConstraints,
) -> Result<LayoutOutput> {
debug_log!(ctx, "Laying out table");
debug_table_layout!(
ctx,
"node_index={}, available_size={:?}, writing_mode={:?}",
node_index,
constraints.available_size,
constraints.writing_mode
);
let table_node = tree
.get(node_index)
.ok_or(LayoutError::InvalidTree)?
.clone();
let table_border_box_width = if let Some(dom_id) = table_node.dom_node_id {
let intrinsic = table_node.intrinsic_sizes.clone().unwrap_or_default();
let containing_block_size = LogicalSize {
width: constraints.available_size.width,
height: constraints.available_size.height,
};
let table_size = crate::solver3::sizing::calculate_used_size_for_node(
ctx.styled_dom,
Some(dom_id),
containing_block_size,
intrinsic,
&table_node.box_props,
ctx.viewport_size,
)?;
table_size.width
} else {
constraints.available_size.width
};
let table_content_box_width = {
let padding_width = table_node.box_props.padding.left + table_node.box_props.padding.right;
let border_width = table_node.box_props.border.left + table_node.box_props.border.right;
(table_border_box_width - padding_width - border_width).max(0.0)
};
debug_table_layout!(ctx, "Table Layout Debug");
debug_table_layout!(ctx, "Node index: {}", node_index);
debug_table_layout!(
ctx,
"Available size from parent: {:.2} x {:.2}",
constraints.available_size.width,
constraints.available_size.height
);
debug_table_layout!(ctx, "Table border-box width: {:.2}", table_border_box_width);
debug_table_layout!(
ctx,
"Table content-box width: {:.2}",
table_content_box_width
);
debug_table_layout!(
ctx,
"Table padding: L={:.2} R={:.2}",
table_node.box_props.padding.left,
table_node.box_props.padding.right
);
debug_table_layout!(
ctx,
"Table border: L={:.2} R={:.2}",
table_node.box_props.border.left,
table_node.box_props.border.right
);
debug_table_layout!(ctx, "=");
let mut table_ctx = analyze_table_structure(tree, node_index, ctx)?;
let table_layout = get_table_layout_property(ctx, &table_node);
table_ctx.use_fixed_layout = matches!(table_layout, LayoutTableLayout::Fixed);
table_ctx.border_collapse = get_border_collapse_property(ctx, &table_node);
table_ctx.border_spacing = get_border_spacing_property(ctx, &table_node);
debug_log!(
ctx,
"Table layout: {:?}, border-collapse: {:?}, border-spacing: {:?}",
table_layout,
table_ctx.border_collapse,
table_ctx.border_spacing
);
if table_ctx.use_fixed_layout {
debug_table_layout!(
ctx,
"FIXED layout: table_content_box_width={:.2}",
table_content_box_width
);
calculate_column_widths_fixed(ctx, &mut table_ctx, table_content_box_width);
} else {
calculate_column_widths_auto_with_width(
&mut table_ctx,
tree,
text_cache,
ctx,
constraints,
table_content_box_width,
)?;
}
debug_table_layout!(ctx, "After column width calculation:");
debug_table_layout!(ctx, " Number of columns: {}", table_ctx.columns.len());
for (i, col) in table_ctx.columns.iter().enumerate() {
debug_table_layout!(
ctx,
" Column {}: width={:.2}",
i,
col.computed_width.unwrap_or(0.0)
);
}
let total_col_width: f32 = table_ctx
.columns
.iter()
.filter_map(|c| c.computed_width)
.sum();
debug_table_layout!(ctx, " Total column width: {:.2}", total_col_width);
calculate_row_heights(&mut table_ctx, tree, text_cache, ctx, constraints)?;
let mut cell_positions =
position_table_cells(&mut table_ctx, tree, ctx, node_index, constraints)?;
let mut table_width: f32 = table_ctx
.columns
.iter()
.filter_map(|col| col.computed_width)
.sum();
let mut table_height: f32 = table_ctx.row_heights.iter().sum();
debug_table_layout!(
ctx,
"After calculate_row_heights: table_height={:.2}, row_heights={:?}",
table_height,
table_ctx.row_heights
);
if table_ctx.border_collapse == StyleBorderCollapse::Separate {
use get_element_font_size;
use get_parent_font_size;
use get_root_font_size;
use PhysicalSize;
use PropertyContext;
use ResolutionContext;
let styled_dom = ctx.styled_dom;
let table_id = tree.nodes[node_index].dom_node_id.unwrap();
let table_state = &styled_dom.styled_nodes.as_container()[table_id].styled_node_state;
let spacing_context = ResolutionContext {
element_font_size: get_element_font_size(styled_dom, table_id, table_state),
parent_font_size: get_parent_font_size(styled_dom, table_id, table_state),
root_font_size: get_root_font_size(styled_dom, table_state),
containing_block_size: PhysicalSize::new(0.0, 0.0),
element_size: None,
viewport_size: PhysicalSize::new(0.0, 0.0),
};
let h_spacing = table_ctx
.border_spacing
.horizontal
.resolve_with_context(&spacing_context, PropertyContext::Other);
let v_spacing = table_ctx
.border_spacing
.vertical
.resolve_with_context(&spacing_context, PropertyContext::Other);
let num_cols = table_ctx.columns.len();
if num_cols > 0 {
table_width += h_spacing * (num_cols + 1) as f32;
}
if table_ctx.num_rows > 0 {
table_height += v_spacing * (table_ctx.num_rows + 1) as f32;
}
}
let caption_side = get_caption_side_property(ctx, &table_node);
let mut caption_height = 0.0;
let mut table_y_offset = 0.0;
if let Some(caption_idx) = table_ctx.caption_index {
debug_log!(
ctx,
"Laying out caption with caption-side: {:?}",
caption_side
);
let caption_constraints = LayoutConstraints {
available_size: LogicalSize {
width: table_width,
height: constraints.available_size.height,
},
writing_mode: constraints.writing_mode,
bfc_state: None, text_align: constraints.text_align,
containing_block_size: constraints.containing_block_size,
available_width_type: Text3AvailableSpace::Definite(table_width),
};
let mut empty_float_cache = std::collections::BTreeMap::new();
let caption_result = layout_formatting_context(
ctx,
tree,
text_cache,
caption_idx,
&caption_constraints,
&mut empty_float_cache,
)?;
caption_height = caption_result.output.overflow_size.height;
let caption_position = match caption_side {
StyleCaptionSide::Top => {
table_y_offset = caption_height;
LogicalPosition { x: 0.0, y: 0.0 }
}
StyleCaptionSide::Bottom => {
LogicalPosition {
x: 0.0,
y: table_height,
}
}
};
cell_positions.insert(caption_idx, caption_position);
debug_log!(
ctx,
"Caption positioned at x={:.2}, y={:.2}, height={:.2}",
caption_position.x,
caption_position.y,
caption_height
);
}
if table_y_offset > 0.0 {
debug_log!(
ctx,
"Adjusting table cells by y offset: {:.2}",
table_y_offset
);
for cell_info in &table_ctx.cells {
if let Some(pos) = cell_positions.get_mut(&cell_info.node_index) {
pos.y += table_y_offset;
}
}
}
let total_height = table_height + caption_height;
debug_table_layout!(ctx, "Final table dimensions:");
debug_table_layout!(ctx, " Content width (columns): {:.2}", table_width);
debug_table_layout!(ctx, " Content height (rows): {:.2}", table_height);
debug_table_layout!(ctx, " Caption height: {:.2}", caption_height);
debug_table_layout!(ctx, " Total height: {:.2}", total_height);
debug_table_layout!(ctx, "End Table Debug");
let output = LayoutOutput {
overflow_size: LogicalSize {
width: table_width,
height: total_height,
},
positions: cell_positions,
baseline: None,
};
Ok(output)
}
fn analyze_table_structure<T: ParsedFontTrait>(
tree: &LayoutTree,
table_index: usize,
ctx: &mut LayoutContext<'_, T>,
) -> Result<TableLayoutContext> {
let mut table_ctx = TableLayoutContext::new();
let table_node = tree.get(table_index).ok_or(LayoutError::InvalidTree)?;
for &child_idx in &table_node.children {
if let Some(child) = tree.get(child_idx) {
if matches!(child.formatting_context, FormattingContext::TableCaption) {
debug_log!(ctx, "Found table caption at index {}", child_idx);
table_ctx.caption_index = Some(child_idx);
continue;
}
if matches!(
child.formatting_context,
FormattingContext::TableColumnGroup
) {
analyze_table_colgroup(tree, child_idx, &mut table_ctx, ctx)?;
continue;
}
match child.formatting_context {
FormattingContext::TableRow => {
analyze_table_row(tree, child_idx, &mut table_ctx, ctx)?;
}
FormattingContext::TableRowGroup => {
for &row_idx in &child.children {
if let Some(row) = tree.get(row_idx) {
if matches!(row.formatting_context, FormattingContext::TableRow) {
analyze_table_row(tree, row_idx, &mut table_ctx, ctx)?;
}
}
}
}
_ => {}
}
}
}
debug_log!(
ctx,
"Table structure: {} rows, {} columns, {} cells{}",
table_ctx.num_rows,
table_ctx.columns.len(),
table_ctx.cells.len(),
if table_ctx.caption_index.is_some() {
", has caption"
} else {
""
}
);
Ok(table_ctx)
}
fn analyze_table_colgroup<T: ParsedFontTrait>(
tree: &LayoutTree,
colgroup_index: usize,
table_ctx: &mut TableLayoutContext,
ctx: &mut LayoutContext<'_, T>,
) -> Result<()> {
let colgroup_node = tree.get(colgroup_index).ok_or(LayoutError::InvalidTree)?;
if is_visibility_collapsed(ctx, colgroup_node) {
debug_log!(
ctx,
"Column group at index {} has visibility:collapse",
colgroup_index
);
}
for &col_idx in &colgroup_node.children {
if let Some(col_node) = tree.get(col_idx) {
if is_visibility_collapsed(ctx, col_node) {
debug_log!(ctx, "Column at index {} has visibility:collapse", col_idx);
}
}
}
Ok(())
}
fn analyze_table_row<T: ParsedFontTrait>(
tree: &LayoutTree,
row_index: usize,
table_ctx: &mut TableLayoutContext,
ctx: &mut LayoutContext<'_, T>,
) -> Result<()> {
let row_node = tree.get(row_index).ok_or(LayoutError::InvalidTree)?;
let row_num = table_ctx.num_rows;
table_ctx.num_rows += 1;
if is_visibility_collapsed(ctx, row_node) {
debug_log!(ctx, "Row {} has visibility:collapse", row_num);
table_ctx.collapsed_rows.insert(row_num);
}
let mut col_index = 0;
for &cell_idx in &row_node.children {
if let Some(cell) = tree.get(cell_idx) {
if matches!(cell.formatting_context, FormattingContext::TableCell) {
let colspan = 1; let rowspan = 1;
let cell_info = TableCellInfo {
node_index: cell_idx,
column: col_index,
colspan,
row: row_num,
rowspan,
};
table_ctx.cells.push(cell_info);
let max_col = col_index + colspan;
while table_ctx.columns.len() < max_col {
table_ctx.columns.push(TableColumnInfo {
min_width: 0.0,
max_width: 0.0,
computed_width: None,
});
}
col_index += colspan;
}
}
}
Ok(())
}
fn calculate_column_widths_fixed<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
table_ctx: &mut TableLayoutContext,
available_width: f32,
) {
debug_table_layout!(
ctx,
"calculate_column_widths_fixed: num_cols={}, available_width={:.2}",
table_ctx.columns.len(),
available_width
);
let num_cols = table_ctx.columns.len();
if num_cols == 0 {
return;
}
let num_visible_cols = num_cols - table_ctx.collapsed_columns.len();
if num_visible_cols == 0 {
for col in &mut table_ctx.columns {
col.computed_width = Some(0.0);
}
return;
}
let col_width = available_width / num_visible_cols as f32;
for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
if table_ctx.collapsed_columns.contains(&col_idx) {
col.computed_width = Some(0.0);
} else {
col.computed_width = Some(col_width);
}
}
}
fn measure_cell_min_content_width<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut crate::font_traits::TextLayoutCache,
cell_index: usize,
constraints: &LayoutConstraints,
) -> Result<f32> {
use crate::text3::cache::AvailableSpace;
let min_constraints = LayoutConstraints {
available_size: LogicalSize {
width: AvailableSpace::MinContent.to_f32_for_layout(),
height: f32::INFINITY,
},
writing_mode: constraints.writing_mode,
bfc_state: None, text_align: constraints.text_align,
containing_block_size: constraints.containing_block_size,
available_width_type: Text3AvailableSpace::MinContent,
};
let mut temp_positions: super::PositionVec = Vec::new();
let mut temp_scrollbar_reflow = false;
let mut temp_float_cache = std::collections::BTreeMap::new();
crate::solver3::cache::calculate_layout_for_subtree(
ctx,
tree,
text_cache,
cell_index,
LogicalPosition::zero(),
min_constraints.available_size,
&mut temp_positions,
&mut temp_scrollbar_reflow,
&mut temp_float_cache,
crate::solver3::cache::ComputeMode::ComputeSize,
)?;
let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
let size = cell_node.used_size.unwrap_or_default();
let padding = &cell_node.box_props.padding;
let border = &cell_node.box_props.border;
let writing_mode = constraints.writing_mode;
let min_width = size.width
+ padding.cross_start(writing_mode)
+ padding.cross_end(writing_mode)
+ border.cross_start(writing_mode)
+ border.cross_end(writing_mode);
Ok(min_width)
}
fn measure_cell_max_content_width<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut crate::font_traits::TextLayoutCache,
cell_index: usize,
constraints: &LayoutConstraints,
) -> Result<f32> {
use crate::text3::cache::AvailableSpace;
let max_constraints = LayoutConstraints {
available_size: LogicalSize {
width: AvailableSpace::MaxContent.to_f32_for_layout(),
height: f32::INFINITY,
},
writing_mode: constraints.writing_mode,
bfc_state: None, text_align: constraints.text_align,
containing_block_size: constraints.containing_block_size,
available_width_type: Text3AvailableSpace::MaxContent,
};
let mut temp_positions: super::PositionVec = Vec::new();
let mut temp_scrollbar_reflow = false;
let mut temp_float_cache = std::collections::BTreeMap::new();
crate::solver3::cache::calculate_layout_for_subtree(
ctx,
tree,
text_cache,
cell_index,
LogicalPosition::zero(),
max_constraints.available_size,
&mut temp_positions,
&mut temp_scrollbar_reflow,
&mut temp_float_cache,
crate::solver3::cache::ComputeMode::ComputeSize,
)?;
let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
let size = cell_node.used_size.unwrap_or_default();
let padding = &cell_node.box_props.padding;
let border = &cell_node.box_props.border;
let writing_mode = constraints.writing_mode;
let max_width = size.width
+ padding.cross_start(writing_mode)
+ padding.cross_end(writing_mode)
+ border.cross_start(writing_mode)
+ border.cross_end(writing_mode);
Ok(max_width)
}
fn calculate_column_widths_auto<T: ParsedFontTrait>(
table_ctx: &mut TableLayoutContext,
tree: &mut LayoutTree,
text_cache: &mut crate::font_traits::TextLayoutCache,
ctx: &mut LayoutContext<'_, T>,
constraints: &LayoutConstraints,
) -> Result<()> {
calculate_column_widths_auto_with_width(
table_ctx,
tree,
text_cache,
ctx,
constraints,
constraints.available_size.width,
)
}
fn calculate_column_widths_auto_with_width<T: ParsedFontTrait>(
table_ctx: &mut TableLayoutContext,
tree: &mut LayoutTree,
text_cache: &mut crate::font_traits::TextLayoutCache,
ctx: &mut LayoutContext<'_, T>,
constraints: &LayoutConstraints,
table_width: f32,
) -> Result<()> {
let num_cols = table_ctx.columns.len();
if num_cols == 0 {
return Ok(());
}
for cell_info in &table_ctx.cells {
if table_ctx.collapsed_columns.contains(&cell_info.column) {
continue;
}
let mut spans_collapsed = false;
for col_offset in 0..cell_info.colspan {
if table_ctx
.collapsed_columns
.contains(&(cell_info.column + col_offset))
{
spans_collapsed = true;
break;
}
}
if spans_collapsed {
continue;
}
let min_width = measure_cell_min_content_width(
ctx,
tree,
text_cache,
cell_info.node_index,
constraints,
)?;
let max_width = measure_cell_max_content_width(
ctx,
tree,
text_cache,
cell_info.node_index,
constraints,
)?;
if cell_info.colspan == 1 {
let col = &mut table_ctx.columns[cell_info.column];
col.min_width = col.min_width.max(min_width);
col.max_width = col.max_width.max(max_width);
} else {
distribute_cell_width_across_columns(
&mut table_ctx.columns,
cell_info.column,
cell_info.colspan,
min_width,
max_width,
&table_ctx.collapsed_columns,
);
}
}
let total_min_width: f32 = table_ctx
.columns
.iter()
.enumerate()
.filter(|(idx, _)| !table_ctx.collapsed_columns.contains(idx))
.map(|(_, c)| c.min_width)
.sum();
let total_max_width: f32 = table_ctx
.columns
.iter()
.enumerate()
.filter(|(idx, _)| !table_ctx.collapsed_columns.contains(idx))
.map(|(_, c)| c.max_width)
.sum();
let available_width = table_width;
debug_table_layout!(
ctx,
"calculate_column_widths_auto: min={:.2}, max={:.2}, table_width={:.2}",
total_min_width,
total_max_width,
table_width
);
if !total_max_width.is_finite() || !available_width.is_finite() {
let num_non_collapsed = table_ctx.columns.len() - table_ctx.collapsed_columns.len();
let width_per_column = if num_non_collapsed > 0 {
available_width / num_non_collapsed as f32
} else {
0.0
};
for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
if table_ctx.collapsed_columns.contains(&col_idx) {
col.computed_width = Some(0.0);
} else {
col.computed_width = Some(col.min_width.max(width_per_column));
}
}
} else if available_width >= total_max_width {
let excess_width = available_width - total_max_width;
let column_info: Vec<(usize, f32, bool)> = table_ctx
.columns
.iter()
.enumerate()
.map(|(idx, c)| (idx, c.max_width, table_ctx.collapsed_columns.contains(&idx)))
.collect();
let total_weight: f32 = column_info.iter()
.filter(|(_, _, is_collapsed)| !is_collapsed)
.map(|(_, max_w, _)| max_w.max(1.0)) .sum();
let num_non_collapsed = column_info
.iter()
.filter(|(_, _, is_collapsed)| !is_collapsed)
.count();
for (col_idx, max_width, is_collapsed) in column_info {
let col = &mut table_ctx.columns[col_idx];
if is_collapsed {
col.computed_width = Some(0.0);
} else {
let weight_factor = if total_weight > 0.0 {
max_width.max(1.0) / total_weight
} else {
1.0 / num_non_collapsed.max(1) as f32
};
let final_width = max_width + (excess_width * weight_factor);
col.computed_width = Some(final_width);
}
}
} else if available_width >= total_min_width {
let scale = if total_max_width > total_min_width {
(available_width - total_min_width) / (total_max_width - total_min_width)
} else {
0.0 };
for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
if table_ctx.collapsed_columns.contains(&col_idx) {
col.computed_width = Some(0.0);
} else {
let interpolated = col.min_width + (col.max_width - col.min_width) * scale;
col.computed_width = Some(interpolated);
}
}
} else {
let scale = available_width / total_min_width;
for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
if table_ctx.collapsed_columns.contains(&col_idx) {
col.computed_width = Some(0.0);
} else {
col.computed_width = Some(col.min_width * scale);
}
}
}
Ok(())
}
fn distribute_cell_width_across_columns(
columns: &mut [TableColumnInfo],
start_col: usize,
colspan: usize,
cell_min_width: f32,
cell_max_width: f32,
collapsed_columns: &std::collections::HashSet<usize>,
) {
let end_col = start_col + colspan;
if end_col > columns.len() {
return;
}
let current_min_total: f32 = columns[start_col..end_col]
.iter()
.enumerate()
.filter(|(idx, _)| !collapsed_columns.contains(&(start_col + idx)))
.map(|(_, c)| c.min_width)
.sum();
let current_max_total: f32 = columns[start_col..end_col]
.iter()
.enumerate()
.filter(|(idx, _)| !collapsed_columns.contains(&(start_col + idx)))
.map(|(_, c)| c.max_width)
.sum();
let num_visible_cols = (start_col..end_col)
.filter(|idx| !collapsed_columns.contains(idx))
.count();
if num_visible_cols == 0 {
return; }
if cell_min_width > current_min_total {
let extra_min = cell_min_width - current_min_total;
let per_col = extra_min / num_visible_cols as f32;
for (idx, col) in columns[start_col..end_col].iter_mut().enumerate() {
if !collapsed_columns.contains(&(start_col + idx)) {
col.min_width += per_col;
}
}
}
if cell_max_width > current_max_total {
let extra_max = cell_max_width - current_max_total;
let per_col = extra_max / num_visible_cols as f32;
for (idx, col) in columns[start_col..end_col].iter_mut().enumerate() {
if !collapsed_columns.contains(&(start_col + idx)) {
col.max_width += per_col;
}
}
}
}
fn layout_cell_for_height<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut crate::font_traits::TextLayoutCache,
cell_index: usize,
cell_width: f32,
constraints: &LayoutConstraints,
) -> Result<f32> {
let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
let cell_dom_id = cell_node.dom_node_id.ok_or(LayoutError::InvalidTree)?;
let has_text_children = cell_dom_id
.az_children(&ctx.styled_dom.node_hierarchy.as_container())
.any(|child_id| {
let node_data = &ctx.styled_dom.node_data.as_container()[child_id];
matches!(node_data.get_node_type(), NodeType::Text(_))
});
debug_table_layout!(
ctx,
"layout_cell_for_height: cell_index={}, has_text_children={}",
cell_index,
has_text_children
);
let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
let padding = &cell_node.box_props.padding;
let border = &cell_node.box_props.border;
let writing_mode = constraints.writing_mode;
let content_width = cell_width
- padding.cross_start(writing_mode)
- padding.cross_end(writing_mode)
- border.cross_start(writing_mode)
- border.cross_end(writing_mode);
debug_table_layout!(
ctx,
"Cell width: border_box={:.2}, content_box={:.2}",
cell_width,
content_width
);
let content_height = if has_text_children {
debug_table_layout!(ctx, "Using IFC to measure text content");
let cell_constraints = LayoutConstraints {
available_size: LogicalSize {
width: content_width, height: f32::INFINITY,
},
writing_mode: constraints.writing_mode,
bfc_state: None,
text_align: constraints.text_align,
containing_block_size: constraints.containing_block_size,
available_width_type: Text3AvailableSpace::Definite(content_width),
};
let output = layout_ifc(ctx, text_cache, tree, cell_index, &cell_constraints)?;
debug_table_layout!(
ctx,
"IFC returned height={:.2}",
output.overflow_size.height
);
output.overflow_size.height
} else {
debug_table_layout!(ctx, "Using regular layout for block children");
let cell_constraints = LayoutConstraints {
available_size: LogicalSize {
width: content_width, height: f32::INFINITY,
},
writing_mode: constraints.writing_mode,
bfc_state: None,
text_align: constraints.text_align,
containing_block_size: constraints.containing_block_size,
available_width_type: Text3AvailableSpace::Definite(content_width),
};
let mut temp_positions: super::PositionVec = Vec::new();
let mut temp_scrollbar_reflow = false;
let mut temp_float_cache = std::collections::BTreeMap::new();
crate::solver3::cache::calculate_layout_for_subtree(
ctx,
tree,
text_cache,
cell_index,
LogicalPosition::zero(),
cell_constraints.available_size,
&mut temp_positions,
&mut temp_scrollbar_reflow,
&mut temp_float_cache,
crate::solver3::cache::ComputeMode::PerformLayout,
)?;
let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
cell_node.used_size.unwrap_or_default().height
};
let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
let padding = &cell_node.box_props.padding;
let border = &cell_node.box_props.border;
let writing_mode = constraints.writing_mode;
let total_height = content_height
+ padding.main_start(writing_mode)
+ padding.main_end(writing_mode)
+ border.main_start(writing_mode)
+ border.main_end(writing_mode);
debug_table_layout!(
ctx,
"Cell total height: cell_index={}, content={:.2}, padding/border={:.2}, total={:.2}",
cell_index,
content_height,
padding.main_start(writing_mode)
+ padding.main_end(writing_mode)
+ border.main_start(writing_mode)
+ border.main_end(writing_mode),
total_height
);
Ok(total_height)
}
fn calculate_row_heights<T: ParsedFontTrait>(
table_ctx: &mut TableLayoutContext,
tree: &mut LayoutTree,
text_cache: &mut crate::font_traits::TextLayoutCache,
ctx: &mut LayoutContext<'_, T>,
constraints: &LayoutConstraints,
) -> Result<()> {
debug_table_layout!(
ctx,
"calculate_row_heights: num_rows={}, available_size={:?}",
table_ctx.num_rows,
constraints.available_size
);
table_ctx.row_heights = vec![0.0; table_ctx.num_rows];
for &row_idx in &table_ctx.collapsed_rows {
if row_idx < table_ctx.row_heights.len() {
table_ctx.row_heights[row_idx] = 0.0;
}
}
for cell_info in &table_ctx.cells {
if table_ctx.collapsed_rows.contains(&cell_info.row) {
continue;
}
let mut cell_width = 0.0;
for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
if let Some(col) = table_ctx.columns.get(col_idx) {
if let Some(width) = col.computed_width {
cell_width += width;
}
}
}
debug_table_layout!(
ctx,
"Cell layout: node_index={}, row={}, col={}, width={:.2}",
cell_info.node_index,
cell_info.row,
cell_info.column,
cell_width
);
let cell_height = layout_cell_for_height(
ctx,
tree,
text_cache,
cell_info.node_index,
cell_width,
constraints,
)?;
debug_table_layout!(
ctx,
"Cell height calculated: node_index={}, height={:.2}",
cell_info.node_index,
cell_height
);
if cell_info.rowspan == 1 {
let current_height = table_ctx.row_heights[cell_info.row];
table_ctx.row_heights[cell_info.row] = current_height.max(cell_height);
}
}
for cell_info in &table_ctx.cells {
if table_ctx.collapsed_rows.contains(&cell_info.row) {
continue;
}
if cell_info.rowspan > 1 {
let mut cell_width = 0.0;
for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
if let Some(col) = table_ctx.columns.get(col_idx) {
if let Some(width) = col.computed_width {
cell_width += width;
}
}
}
let cell_height = layout_cell_for_height(
ctx,
tree,
text_cache,
cell_info.node_index,
cell_width,
constraints,
)?;
let end_row = cell_info.row + cell_info.rowspan;
let current_total: f32 = table_ctx.row_heights[cell_info.row..end_row]
.iter()
.enumerate()
.filter(|(idx, _)| !table_ctx.collapsed_rows.contains(&(cell_info.row + idx)))
.map(|(_, height)| height)
.sum();
if cell_height > current_total {
let extra_height = cell_height - current_total;
let non_collapsed_rows = (cell_info.row..end_row)
.filter(|row_idx| !table_ctx.collapsed_rows.contains(row_idx))
.count();
if non_collapsed_rows > 0 {
let per_row = extra_height / non_collapsed_rows as f32;
for row_idx in cell_info.row..end_row {
if !table_ctx.collapsed_rows.contains(&row_idx) {
table_ctx.row_heights[row_idx] += per_row;
}
}
}
}
}
}
for &row_idx in &table_ctx.collapsed_rows {
if row_idx < table_ctx.row_heights.len() {
table_ctx.row_heights[row_idx] = 0.0;
}
}
Ok(())
}
fn position_table_cells<T: ParsedFontTrait>(
table_ctx: &mut TableLayoutContext,
tree: &mut LayoutTree,
ctx: &mut LayoutContext<'_, T>,
table_index: usize,
constraints: &LayoutConstraints,
) -> Result<BTreeMap<usize, LogicalPosition>> {
debug_log!(ctx, "Positioning table cells in grid");
let mut positions = BTreeMap::new();
let (h_spacing, v_spacing) = if table_ctx.border_collapse == StyleBorderCollapse::Separate {
let styled_dom = ctx.styled_dom;
let table_id = tree.nodes[table_index].dom_node_id.unwrap();
let table_state = &styled_dom.styled_nodes.as_container()[table_id].styled_node_state;
let spacing_context = ResolutionContext {
element_font_size: get_element_font_size(styled_dom, table_id, table_state),
parent_font_size: get_parent_font_size(styled_dom, table_id, table_state),
root_font_size: get_root_font_size(styled_dom, table_state),
containing_block_size: PhysicalSize::new(0.0, 0.0),
element_size: None,
viewport_size: PhysicalSize::new(0.0, 0.0), };
let h = table_ctx
.border_spacing
.horizontal
.resolve_with_context(&spacing_context, PropertyContext::Other);
let v = table_ctx
.border_spacing
.vertical
.resolve_with_context(&spacing_context, PropertyContext::Other);
(h, v)
} else {
(0.0, 0.0)
};
debug_log!(
ctx,
"Border spacing: h={:.2}, v={:.2}",
h_spacing,
v_spacing
);
let mut col_positions = vec![0.0; table_ctx.columns.len()];
let mut x_offset = h_spacing; for (i, col) in table_ctx.columns.iter().enumerate() {
col_positions[i] = x_offset;
if let Some(width) = col.computed_width {
x_offset += width + h_spacing; }
}
let mut row_positions = vec![0.0; table_ctx.num_rows];
let mut y_offset = v_spacing; for (i, &height) in table_ctx.row_heights.iter().enumerate() {
row_positions[i] = y_offset;
y_offset += height + v_spacing; }
for cell_info in &table_ctx.cells {
let cell_node = tree
.get_mut(cell_info.node_index)
.ok_or(LayoutError::InvalidTree)?;
let x = col_positions.get(cell_info.column).copied().unwrap_or(0.0);
let y = row_positions.get(cell_info.row).copied().unwrap_or(0.0);
let mut width = 0.0;
debug_info!(
ctx,
"[position_table_cells] Cell {}: calculating width from cols {}..{}",
cell_info.node_index,
cell_info.column,
cell_info.column + cell_info.colspan
);
for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
if let Some(col) = table_ctx.columns.get(col_idx) {
debug_info!(
ctx,
"[position_table_cells] Col {}: computed_width={:?}",
col_idx,
col.computed_width
);
if let Some(col_width) = col.computed_width {
width += col_width;
if col_idx < cell_info.column + cell_info.colspan - 1 {
width += h_spacing;
}
} else {
debug_info!(
ctx,
"[position_table_cells] WARN: Col {} has NO computed_width!",
col_idx
);
}
} else {
debug_info!(
ctx,
"[position_table_cells] WARN: Col {} not found in table_ctx.columns!",
col_idx
);
}
}
let mut height = 0.0;
let end_row = cell_info.row + cell_info.rowspan;
for row_idx in cell_info.row..end_row {
if let Some(&row_height) = table_ctx.row_heights.get(row_idx) {
height += row_height;
if row_idx < end_row - 1 {
height += v_spacing;
}
}
}
let writing_mode = constraints.writing_mode;
debug_info!(
ctx,
"[position_table_cells] Cell {}: BEFORE from_main_cross: width={}, height={}, \
writing_mode={:?}",
cell_info.node_index,
width,
height,
writing_mode
);
cell_node.used_size = Some(LogicalSize::from_main_cross(height, width, writing_mode));
debug_info!(
ctx,
"[position_table_cells] Cell {}: AFTER from_main_cross: used_size={:?}",
cell_info.node_index,
cell_node.used_size
);
debug_info!(
ctx,
"[position_table_cells] Cell {}: setting used_size to {}x{} (row_heights={:?})",
cell_info.node_index,
width,
height,
table_ctx.row_heights
);
if let Some(ref cached_layout) = cell_node.inline_layout_result {
let inline_result = &cached_layout.layout;
use StyleVerticalAlign;
let vertical_align = if let Some(dom_id) = cell_node.dom_node_id {
let node_state = StyledNodeState::default();
match get_vertical_align_property(ctx.styled_dom, dom_id, &node_state) {
MultiValue::Exact(v) => v,
_ => StyleVerticalAlign::Top,
}
} else {
StyleVerticalAlign::Top
};
let content_bounds = inline_result.bounds();
let content_height = content_bounds.height;
let padding = &cell_node.box_props.padding;
let border = &cell_node.box_props.border;
let content_box_height = height
- padding.main_start(writing_mode)
- padding.main_end(writing_mode)
- border.main_start(writing_mode)
- border.main_end(writing_mode);
let align_factor = match vertical_align {
StyleVerticalAlign::Top => 0.0,
StyleVerticalAlign::Middle => 0.5,
StyleVerticalAlign::Bottom => 1.0,
StyleVerticalAlign::Baseline
| StyleVerticalAlign::Sub
| StyleVerticalAlign::Superscript
| StyleVerticalAlign::TextTop
| StyleVerticalAlign::TextBottom => 0.5,
};
let y_offset = (content_box_height - content_height) * align_factor;
debug_info!(
ctx,
"[position_table_cells] Cell {}: vertical-align={:?}, border_box_height={}, \
content_box_height={}, content_height={}, y_offset={}",
cell_info.node_index,
vertical_align,
height,
content_box_height,
content_height,
y_offset
);
if y_offset.abs() > 0.01 {
use std::sync::Arc;
use crate::text3::cache::{PositionedItem, UnifiedLayout};
let adjusted_items: Vec<PositionedItem> = inline_result
.items
.iter()
.map(|item| PositionedItem {
item: item.item.clone(),
position: crate::text3::cache::Point {
x: item.position.x,
y: item.position.y + y_offset,
},
line_index: item.line_index,
})
.collect();
let adjusted_layout = UnifiedLayout {
items: adjusted_items,
overflow: inline_result.overflow.clone(),
};
cell_node.inline_layout_result = Some(CachedInlineLayout::new(
Arc::new(adjusted_layout),
cached_layout.available_width,
cached_layout.has_floats,
));
}
}
let position = LogicalPosition::from_main_cross(y, x, writing_mode);
positions.insert(cell_info.node_index, position);
debug_log!(
ctx,
"Cell at row={}, col={}: pos=({:.2}, {:.2}), size=({:.2}x{:.2})",
cell_info.row,
cell_info.column,
x,
y,
width,
height
);
}
Ok(positions)
}
fn collect_and_measure_inline_content<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
text_cache: &mut TextLayoutCache,
tree: &mut LayoutTree,
ifc_root_index: usize,
constraints: &LayoutConstraints,
) -> Result<(Vec<InlineContent>, HashMap<ContentIndex, usize>)> {
use crate::solver3::layout_tree::{IfcId, IfcMembership};
use crate::text3::cache::InlineContent;
let result = collect_and_measure_inline_content_impl(ctx, text_cache, tree, ifc_root_index, constraints)?;
Ok(result)
}
fn collect_and_measure_inline_content_impl<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
text_cache: &mut TextLayoutCache,
tree: &mut LayoutTree,
ifc_root_index: usize,
constraints: &LayoutConstraints,
) -> Result<(Vec<InlineContent>, HashMap<ContentIndex, usize>)> {
use crate::solver3::layout_tree::{IfcId, IfcMembership};
debug_ifc_layout!(
ctx,
"collect_and_measure_inline_content: node_index={}",
ifc_root_index
);
let ifc_id = IfcId::unique();
if let Some(ifc_root_node) = tree.get_mut(ifc_root_index) {
ifc_root_node.ifc_id = Some(ifc_id);
}
let mut content = Vec::new();
let mut child_map = HashMap::new();
let mut current_run_index: u32 = 0;
let ifc_root_node = tree.get(ifc_root_index).ok_or(LayoutError::InvalidTree)?;
let is_anonymous = ifc_root_node.dom_node_id.is_none();
let ifc_root_dom_id = match ifc_root_node.dom_node_id {
Some(id) => id,
None => {
let parent_dom_id = ifc_root_node
.parent
.and_then(|p| tree.get(p))
.and_then(|n| n.dom_node_id);
if let Some(id) = parent_dom_id {
id
} else {
match ifc_root_node
.children
.iter()
.filter_map(|&child_idx| tree.get(child_idx))
.filter_map(|n| n.dom_node_id)
.next()
{
Some(id) => id,
None => {
debug_warning!(ctx, "IFC root and all ancestors/children have no DOM ID");
return Ok((content, child_map));
}
}
}
}
};
let children: Vec<_> = ifc_root_node.children.clone();
drop(ifc_root_node);
debug_ifc_layout!(
ctx,
"Node {} has {} layout children, is_anonymous={}",
ifc_root_index,
children.len(),
is_anonymous
);
if is_anonymous {
for (item_idx, &child_index) in children.iter().enumerate() {
let content_index = ContentIndex {
run_index: ifc_root_index as u32,
item_index: item_idx as u32,
};
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let Some(dom_id) = child_node.dom_node_id else {
debug_warning!(
ctx,
"Anonymous IFC child at index {} has no DOM ID",
child_index
);
continue;
};
let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
if let NodeType::Text(ref text_content) = node_data.get_node_type() {
debug_info!(
ctx,
"[collect_and_measure_inline_content] OK: Found text node (DOM {:?}) in anonymous wrapper: '{}'",
dom_id,
text_content.as_str()
);
let style = Arc::new(get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref()));
let text_items = split_text_for_whitespace(
ctx.styled_dom,
dom_id,
text_content.as_str(),
style,
);
content.extend(text_items);
child_map.insert(content_index, child_index);
drop(child_node);
if let Some(child_node_mut) = tree.get_mut(child_index) {
child_node_mut.ifc_membership = Some(IfcMembership {
ifc_id,
ifc_root_layout_index: ifc_root_index,
run_index: current_run_index,
});
}
current_run_index += 1;
continue;
}
let display = get_display_property(ctx.styled_dom, Some(dom_id)).unwrap_or_default();
if display != LayoutDisplay::Inline {
let intrinsic_size = child_node.intrinsic_sizes.clone().unwrap_or_default();
let box_props = child_node.box_props.clone();
let styled_node_state = ctx
.styled_dom
.styled_nodes
.as_container()
.get(dom_id)
.map(|n| n.styled_node_state.clone())
.unwrap_or_default();
let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
ctx.styled_dom,
Some(dom_id),
constraints.containing_block_size,
intrinsic_size,
&box_props,
ctx.viewport_size,
)?;
let writing_mode = get_writing_mode(ctx.styled_dom, dom_id, &styled_node_state)
.unwrap_or_default();
let content_box_size = box_props.inner_size(tentative_size, writing_mode);
let child_constraints = LayoutConstraints {
available_size: LogicalSize::new(content_box_size.width, f32::INFINITY),
writing_mode,
bfc_state: None,
text_align: TextAlign::Start,
containing_block_size: constraints.containing_block_size,
available_width_type: Text3AvailableSpace::Definite(content_box_size.width),
};
drop(child_node);
let mut empty_float_cache = std::collections::BTreeMap::new();
let layout_result = layout_formatting_context(
ctx,
tree,
text_cache,
child_index,
&child_constraints,
&mut empty_float_cache,
)?;
let css_height = get_css_height(ctx.styled_dom, dom_id, &styled_node_state);
let final_height = match css_height.unwrap_or_default() {
LayoutHeight::Auto => {
let content_height = layout_result.output.overflow_size.height;
content_height
+ box_props.padding.main_sum(writing_mode)
+ box_props.border.main_sum(writing_mode)
}
_ => tentative_size.height,
};
let final_size = LogicalSize::new(tentative_size.width, final_height);
tree.get_mut(child_index).unwrap().used_size = Some(final_size);
let baseline_offset = layout_result.output.baseline.unwrap_or(final_height);
let margin = &box_props.margin;
let margin_box_width = final_size.width + margin.left + margin.right;
let margin_box_height = final_size.height + margin.top + margin.bottom;
let shape_content_index = ContentIndex {
run_index: content.len() as u32,
item_index: 0,
};
content.push(InlineContent::Shape(InlineShape {
shape_def: ShapeDefinition::Rectangle {
size: crate::text3::cache::Size {
width: margin_box_width,
height: margin_box_height,
},
corner_radius: None,
},
fill: None,
stroke: None,
baseline_offset: baseline_offset + margin.top,
alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, dom_id),
source_node_id: Some(dom_id),
}));
child_map.insert(shape_content_index, child_index);
} else {
let span_style = get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref());
collect_inline_span_recursive(
ctx,
tree,
dom_id,
span_style,
&mut content,
&mut child_map,
&children,
constraints,
)?;
}
}
return Ok((content, child_map));
}
let ifc_root_node = tree.get(ifc_root_index).ok_or(LayoutError::InvalidTree)?;
let mut list_item_dom_id: Option<NodeId> = None;
if let Some(dom_id) = ifc_root_node.dom_node_id {
use crate::solver3::getters::get_display_property;
if let MultiValue::Exact(display) = get_display_property(ctx.styled_dom, Some(dom_id)) {
use LayoutDisplay;
if display == LayoutDisplay::ListItem {
debug_ifc_layout!(ctx, "IFC root NodeId({:?}) is list-item", dom_id);
list_item_dom_id = Some(dom_id);
}
}
}
if list_item_dom_id.is_none() {
if let Some(parent_idx) = ifc_root_node.parent {
if let Some(parent_node) = tree.get(parent_idx) {
if let Some(parent_dom_id) = parent_node.dom_node_id {
use crate::solver3::getters::get_display_property;
if let MultiValue::Exact(display) = get_display_property(ctx.styled_dom, Some(parent_dom_id)) {
use LayoutDisplay;
if display == LayoutDisplay::ListItem {
debug_ifc_layout!(
ctx,
"IFC root parent NodeId({:?}) is list-item",
parent_dom_id
);
list_item_dom_id = Some(parent_dom_id);
}
}
}
}
}
}
if let Some(list_dom_id) = list_item_dom_id {
debug_ifc_layout!(
ctx,
"Found list-item (NodeId({:?})), generating marker",
list_dom_id
);
let list_item_layout_idx = tree
.nodes
.iter()
.enumerate()
.find(|(_, node)| {
node.dom_node_id == Some(list_dom_id) && node.pseudo_element.is_none()
})
.map(|(idx, _)| idx);
if let Some(list_idx) = list_item_layout_idx {
let list_item_node = tree.get(list_idx).ok_or(LayoutError::InvalidTree)?;
let marker_idx = list_item_node
.children
.iter()
.find(|&&child_idx| {
tree.get(child_idx)
.map(|child| child.pseudo_element == Some(PseudoElement::Marker))
.unwrap_or(false)
})
.copied();
if let Some(marker_idx) = marker_idx {
debug_ifc_layout!(ctx, "Found ::marker pseudo-element at index {}", marker_idx);
let list_dom_id_for_style = tree
.get(marker_idx)
.and_then(|n| n.dom_node_id)
.unwrap_or(list_dom_id);
let list_style_position =
get_list_style_position(ctx.styled_dom, Some(list_dom_id));
let position_outside =
matches!(list_style_position, StyleListStylePosition::Outside);
debug_ifc_layout!(
ctx,
"List marker list-style-position: {:?} (outside={})",
list_style_position,
position_outside
);
let base_style =
Arc::new(get_style_properties(ctx.styled_dom, list_dom_id_for_style, ctx.system_style.as_ref()));
let marker_segments = generate_list_marker_segments(
tree,
ctx.styled_dom,
marker_idx, ctx.counters,
base_style,
ctx.debug_messages,
);
debug_ifc_layout!(
ctx,
"Generated {} list marker segments",
marker_segments.len()
);
for segment in marker_segments {
content.push(InlineContent::Marker {
run: segment,
position_outside,
});
}
} else {
debug_ifc_layout!(
ctx,
"WARNING: List-item at index {} has no ::marker pseudo-element",
list_idx
);
}
}
}
drop(ifc_root_node);
let node_hier_item = &ctx.styled_dom.node_hierarchy.as_container()[ifc_root_dom_id];
debug_info!(
ctx,
"[collect_and_measure_inline_content] DEBUG: node_hier_item.first_child={:?}, \
last_child={:?}",
node_hier_item.first_child_id(ifc_root_dom_id),
node_hier_item.last_child_id()
);
let dom_children: Vec<NodeId> = ifc_root_dom_id
.az_children(&ctx.styled_dom.node_hierarchy.as_container())
.collect();
let ifc_root_node_data = &ctx.styled_dom.node_data.as_container()[ifc_root_dom_id];
if let NodeType::Text(ref text_content) = ifc_root_node_data.get_node_type() {
let style = Arc::new(get_style_properties(ctx.styled_dom, ifc_root_dom_id, ctx.system_style.as_ref()));
let text_items = split_text_for_whitespace(
ctx.styled_dom,
ifc_root_dom_id,
text_content.as_str(),
style,
);
content.extend(text_items);
return Ok((content, child_map));
}
let ifc_root_node_type = match ifc_root_node_data.get_node_type() {
NodeType::Div => "Div",
NodeType::Text(_) => "Text",
NodeType::Body => "Body",
_ => "Other",
};
debug_info!(
ctx,
"[collect_and_measure_inline_content] IFC root has {} DOM children",
dom_children.len()
);
for (item_idx, &dom_child_id) in dom_children.iter().enumerate() {
let content_index = ContentIndex {
run_index: ifc_root_index as u32,
item_index: item_idx as u32,
};
let node_data = &ctx.styled_dom.node_data.as_container()[dom_child_id];
if let NodeType::Text(ref text_content) = node_data.get_node_type() {
debug_info!(
ctx,
"[collect_and_measure_inline_content] OK: Found text node (DOM child {:?}): '{}'",
dom_child_id,
text_content.as_str()
);
let style = Arc::new(get_style_properties(ctx.styled_dom, dom_child_id, ctx.system_style.as_ref()));
let text_items = split_text_for_whitespace(
ctx.styled_dom,
dom_child_id,
text_content.as_str(),
style,
);
content.extend(text_items);
if let Some(&layout_idx) = tree.dom_to_layout.get(&dom_child_id).and_then(|v| v.first()) {
if let Some(text_layout_node) = tree.get_mut(layout_idx) {
text_layout_node.ifc_membership = Some(IfcMembership {
ifc_id,
ifc_root_layout_index: ifc_root_index,
run_index: current_run_index,
});
}
}
current_run_index += 1;
continue;
}
let child_index = children
.iter()
.find(|&&idx| {
tree.get(idx)
.and_then(|n| n.dom_node_id)
.map(|id| id == dom_child_id)
.unwrap_or(false)
})
.copied();
let Some(child_index) = child_index else {
debug_info!(
ctx,
"[collect_and_measure_inline_content] WARN: DOM child {:?} has no layout node",
dom_child_id
);
continue;
};
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let dom_id = child_node.dom_node_id.unwrap();
let display = get_display_property(ctx.styled_dom, Some(dom_id)).unwrap_or_default();
if display != LayoutDisplay::Inline {
let intrinsic_size = child_node.intrinsic_sizes.clone().unwrap_or_default();
let box_props = child_node.box_props.clone();
let styled_node_state = ctx
.styled_dom
.styled_nodes
.as_container()
.get(dom_id)
.map(|n| n.styled_node_state.clone())
.unwrap_or_default();
let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
ctx.styled_dom,
Some(dom_id),
constraints.containing_block_size,
intrinsic_size,
&box_props,
ctx.viewport_size,
)?;
let writing_mode =
get_writing_mode(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
let content_box_size = box_props.inner_size(tentative_size, writing_mode);
debug_info!(
ctx,
"[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
tentative_border_box={:?}, content_box={:?}",
dom_id,
tentative_size,
content_box_size
);
let child_constraints = LayoutConstraints {
available_size: LogicalSize::new(content_box_size.width, f32::INFINITY),
writing_mode,
bfc_state: None,
text_align: TextAlign::Start,
containing_block_size: constraints.containing_block_size,
available_width_type: Text3AvailableSpace::Definite(content_box_size.width),
};
drop(child_node);
let mut empty_float_cache = std::collections::BTreeMap::new();
let layout_result = layout_formatting_context(
ctx,
tree,
text_cache,
child_index,
&child_constraints,
&mut empty_float_cache,
)?;
let css_height = get_css_height(ctx.styled_dom, dom_id, &styled_node_state);
let final_height = match css_height.clone().unwrap_or_default() {
LayoutHeight::Auto => {
let content_height = layout_result.output.overflow_size.height;
content_height
+ box_props.padding.main_sum(writing_mode)
+ box_props.border.main_sum(writing_mode)
}
_ => tentative_size.height,
};
debug_info!(
ctx,
"[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
layout_content_height={}, css_height={:?}, final_border_box_height={}",
dom_id,
layout_result.output.overflow_size.height,
css_height,
final_height
);
let final_size = LogicalSize::new(tentative_size.width, final_height);
tree.get_mut(child_index).unwrap().used_size = Some(final_size);
let baseline_from_top = layout_result.output.baseline;
let baseline_offset = match baseline_from_top {
Some(baseline_y) => {
let content_box_top = box_props.padding.top + box_props.border.top;
let baseline_from_border_box_top = baseline_y + content_box_top;
(final_height - baseline_from_border_box_top).max(0.0)
}
None => {
0.0
}
};
debug_info!(
ctx,
"[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
baseline_from_top={:?}, final_height={}, baseline_offset_from_bottom={}",
dom_id,
baseline_from_top,
final_height,
baseline_offset
);
let margin = &box_props.margin;
let margin_box_width = final_size.width + margin.left + margin.right;
let margin_box_height = final_size.height + margin.top + margin.bottom;
let shape_content_index = ContentIndex {
run_index: content.len() as u32,
item_index: 0,
};
content.push(InlineContent::Shape(InlineShape {
shape_def: ShapeDefinition::Rectangle {
size: crate::text3::cache::Size {
width: margin_box_width,
height: margin_box_height,
},
corner_radius: None,
},
fill: None,
stroke: None,
baseline_offset: baseline_offset + margin.top,
alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, dom_id),
source_node_id: Some(dom_id),
}));
child_map.insert(shape_content_index, child_index);
} else if let NodeType::Image(image_ref) =
ctx.styled_dom.node_data.as_container()[dom_id].get_node_type()
{
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let box_props = child_node.box_props.clone();
let intrinsic_size = child_node
.intrinsic_sizes
.clone()
.unwrap_or(IntrinsicSizes {
max_content_width: 50.0,
max_content_height: 50.0,
..Default::default()
});
let styled_node_state = ctx
.styled_dom
.styled_nodes
.as_container()
.get(dom_id)
.map(|n| n.styled_node_state.clone())
.unwrap_or_default();
let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
ctx.styled_dom,
Some(dom_id),
constraints.containing_block_size,
intrinsic_size.clone(),
&box_props,
ctx.viewport_size,
)?;
drop(child_node);
let final_size = LogicalSize::new(tentative_size.width, tentative_size.height);
tree.get_mut(child_index).unwrap().used_size = Some(final_size);
let display_width = if final_size.width > 0.0 {
Some(final_size.width)
} else {
None
};
let display_height = if final_size.height > 0.0 {
Some(final_size.height)
} else {
None
};
content.push(InlineContent::Image(InlineImage {
source: ImageSource::Ref(image_ref.clone()),
intrinsic_size: crate::text3::cache::Size {
width: intrinsic_size.max_content_width,
height: intrinsic_size.max_content_height,
},
display_size: if display_width.is_some() || display_height.is_some() {
Some(crate::text3::cache::Size {
width: display_width.unwrap_or(intrinsic_size.max_content_width),
height: display_height.unwrap_or(intrinsic_size.max_content_height),
})
} else {
None
},
baseline_offset: 0.0,
alignment: crate::text3::cache::VerticalAlign::Baseline,
object_fit: ObjectFit::Fill,
}));
let image_content_index = ContentIndex {
run_index: (content.len() - 1) as u32, item_index: 0,
};
child_map.insert(image_content_index, child_index);
} else {
debug_info!(
ctx,
"[collect_and_measure_inline_content] Found inline span (DOM {:?}), recursing",
dom_id
);
let span_style = get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref());
collect_inline_span_recursive(
ctx,
tree,
dom_id,
span_style,
&mut content,
&mut child_map,
&children,
constraints,
)?;
}
}
Ok((content, child_map))
}
fn collect_inline_span_recursive<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
span_dom_id: NodeId,
span_style: StyleProperties,
content: &mut Vec<InlineContent>,
child_map: &mut HashMap<ContentIndex, usize>,
parent_children: &[usize], constraints: &LayoutConstraints,
) -> Result<()> {
debug_info!(
ctx,
"[collect_inline_span_recursive] Processing inline span {:?}",
span_dom_id
);
let span_dom_children: Vec<NodeId> = span_dom_id
.az_children(&ctx.styled_dom.node_hierarchy.as_container())
.collect();
debug_info!(
ctx,
"[collect_inline_span_recursive] Span has {} DOM children",
span_dom_children.len()
);
for &child_dom_id in &span_dom_children {
let node_data = &ctx.styled_dom.node_data.as_container()[child_dom_id];
if let NodeType::Text(ref text_content) = node_data.get_node_type() {
debug_info!(
ctx,
"[collect_inline_span_recursive] ✓ Found text in span: '{}'",
text_content.as_str()
);
let text_items = split_text_for_whitespace(
ctx.styled_dom,
child_dom_id,
text_content.as_str(),
Arc::new(span_style.clone()),
);
content.extend(text_items);
continue;
}
let child_display =
get_display_property(ctx.styled_dom, Some(child_dom_id)).unwrap_or_default();
let child_index = parent_children
.iter()
.find(|&&idx| {
tree.get(idx)
.and_then(|n| n.dom_node_id)
.map(|id| id == child_dom_id)
.unwrap_or(false)
})
.copied();
match child_display {
LayoutDisplay::Inline => {
debug_info!(
ctx,
"[collect_inline_span_recursive] Found nested inline span {:?}",
child_dom_id
);
let child_style = get_style_properties(ctx.styled_dom, child_dom_id, ctx.system_style.as_ref());
collect_inline_span_recursive(
ctx,
tree,
child_dom_id,
child_style,
content,
child_map,
parent_children,
constraints,
)?;
}
LayoutDisplay::InlineBlock => {
let Some(child_index) = child_index else {
debug_info!(
ctx,
"[collect_inline_span_recursive] WARNING: inline-block {:?} has no layout \
node",
child_dom_id
);
continue;
};
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let intrinsic_size = child_node.intrinsic_sizes.clone().unwrap_or_default();
let width = intrinsic_size.max_content_width;
let styled_node_state = ctx
.styled_dom
.styled_nodes
.as_container()
.get(child_dom_id)
.map(|n| n.styled_node_state.clone())
.unwrap_or_default();
let writing_mode =
get_writing_mode(ctx.styled_dom, child_dom_id, &styled_node_state)
.unwrap_or_default();
let child_constraints = LayoutConstraints {
available_size: LogicalSize::new(width, f32::INFINITY),
writing_mode,
bfc_state: None,
text_align: TextAlign::Start,
containing_block_size: constraints.containing_block_size,
available_width_type: Text3AvailableSpace::Definite(width),
};
drop(child_node);
let mut empty_float_cache = std::collections::BTreeMap::new();
let layout_result = layout_formatting_context(
ctx,
tree,
&mut TextLayoutCache::default(),
child_index,
&child_constraints,
&mut empty_float_cache,
)?;
let final_height = layout_result.output.overflow_size.height;
let final_size = LogicalSize::new(width, final_height);
tree.get_mut(child_index).unwrap().used_size = Some(final_size);
let baseline_offset = layout_result.output.baseline.unwrap_or(final_height);
content.push(InlineContent::Shape(InlineShape {
shape_def: ShapeDefinition::Rectangle {
size: crate::text3::cache::Size {
width,
height: final_height,
},
corner_radius: None,
},
fill: None,
stroke: None,
baseline_offset,
alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, child_dom_id),
source_node_id: Some(child_dom_id),
}));
debug_info!(
ctx,
"[collect_inline_span_recursive] Added inline-block shape {}x{}",
width,
final_height
);
}
_ => {
debug_info!(
ctx,
"[collect_inline_span_recursive] WARNING: Unsupported display type {:?} \
inside inline span",
child_display
);
}
}
}
Ok(())
}
fn position_floated_child(
_child_index: usize,
child_margin_box_size: LogicalSize,
float_type: LayoutFloat,
constraints: &LayoutConstraints,
_bfc_content_box: LogicalRect,
current_main_offset: f32,
floating_context: &mut FloatingContext,
) -> Result<LogicalPosition> {
let wm = constraints.writing_mode;
let child_main_size = child_margin_box_size.main(wm);
let child_cross_size = child_margin_box_size.cross(wm);
let bfc_cross_size = constraints.available_size.cross(wm);
let mut placement_main_offset = current_main_offset;
loop {
let (available_cross_start, available_cross_end) = floating_context
.available_line_box_space(
placement_main_offset,
placement_main_offset + child_main_size,
bfc_cross_size,
wm,
);
let available_cross_width = available_cross_end - available_cross_start;
if child_cross_size <= available_cross_width {
let final_cross_pos = match float_type {
LayoutFloat::Left => available_cross_start,
LayoutFloat::Right => available_cross_end - child_cross_size,
LayoutFloat::None => unreachable!(),
};
let final_pos =
LogicalPosition::from_main_cross(placement_main_offset, final_cross_pos, wm);
let new_float_box = FloatBox {
kind: float_type,
rect: LogicalRect::new(final_pos, child_margin_box_size),
margin: EdgeSizes::default(), };
floating_context.floats.push(new_float_box);
return Ok(final_pos);
} else {
let mut next_main_offset = f32::INFINITY;
for existing_float in &floating_context.floats {
let float_main_start = existing_float.rect.origin.main(wm);
let float_main_end = float_main_start + existing_float.rect.size.main(wm);
if placement_main_offset < float_main_end {
next_main_offset = next_main_offset.min(float_main_end);
}
}
if next_main_offset.is_infinite() {
return Err(LayoutError::PositioningFailed);
}
placement_main_offset = next_main_offset;
}
}
}
fn get_float_property(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutFloat {
let Some(id) = dom_id else {
return LayoutFloat::None;
};
let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
get_float(styled_dom, id, node_state).unwrap_or(LayoutFloat::None)
}
fn get_clear_property(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutClear {
let Some(id) = dom_id else {
return LayoutClear::None;
};
let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
get_clear(styled_dom, id, node_state).unwrap_or(LayoutClear::None)
}
pub fn check_scrollbar_necessity(
content_size: LogicalSize,
container_size: LogicalSize,
overflow_x: OverflowBehavior,
overflow_y: OverflowBehavior,
scrollbar_width_px: f32,
) -> ScrollbarRequirements {
const EPSILON: f32 = 1.0;
let mut needs_horizontal = match overflow_x {
OverflowBehavior::Visible | OverflowBehavior::Hidden | OverflowBehavior::Clip => false,
OverflowBehavior::Scroll => true,
OverflowBehavior::Auto => content_size.width > container_size.width + EPSILON,
};
let mut needs_vertical = match overflow_y {
OverflowBehavior::Visible | OverflowBehavior::Hidden | OverflowBehavior::Clip => false,
OverflowBehavior::Scroll => true,
OverflowBehavior::Auto => content_size.height > container_size.height + EPSILON,
};
if scrollbar_width_px > 0.0 {
if needs_vertical && !needs_horizontal && overflow_x == OverflowBehavior::Auto {
if content_size.width > (container_size.width - scrollbar_width_px) + EPSILON {
needs_horizontal = true;
}
}
if needs_horizontal && !needs_vertical && overflow_y == OverflowBehavior::Auto {
if content_size.height > (container_size.height - scrollbar_width_px) + EPSILON {
needs_vertical = true;
}
}
}
ScrollbarRequirements {
needs_horizontal,
needs_vertical,
scrollbar_width: if needs_vertical {
scrollbar_width_px
} else {
0.0
},
scrollbar_height: if needs_horizontal {
scrollbar_width_px
} else {
0.0
},
}
}
pub(crate) fn collapse_margins(a: f32, b: f32) -> f32 {
if a.is_sign_positive() && b.is_sign_positive() {
a.max(b)
} else if a.is_sign_negative() && b.is_sign_negative() {
a.min(b)
} else {
a + b
}
}
fn advance_pen_with_margin_collapse(
pen: &mut f32,
last_margin_bottom: f32,
current_margin_top: f32,
) -> f32 {
let collapsed_margin = collapse_margins(last_margin_bottom, current_margin_top);
*pen += collapsed_margin;
collapsed_margin
}
fn has_margin_collapse_blocker(
box_props: &crate::solver3::geometry::BoxProps,
writing_mode: LayoutWritingMode,
check_start: bool, ) -> bool {
if check_start {
let border_start = box_props.border.main_start(writing_mode);
let padding_start = box_props.padding.main_start(writing_mode);
border_start > 0.0 || padding_start > 0.0
} else {
let border_end = box_props.border.main_end(writing_mode);
let padding_end = box_props.padding.main_end(writing_mode);
border_end > 0.0 || padding_end > 0.0
}
}
fn is_empty_block(node: &LayoutNode) -> bool {
if !node.children.is_empty() {
return false;
}
if node.inline_layout_result.is_some() {
return false;
}
if let Some(size) = node.used_size {
if size.height > 0.0 {
return false;
}
}
true
}
fn generate_list_marker_text(
tree: &LayoutTree,
styled_dom: &StyledDom,
marker_index: usize,
counters: &BTreeMap<(usize, String), i32>,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> String {
use crate::solver3::counters::format_counter;
let marker_node = match tree.get(marker_index) {
Some(n) => n,
None => return String::new(),
};
if marker_node.pseudo_element != Some(PseudoElement::Marker) {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::warning(format!(
"[generate_list_marker_text] WARNING: Node {} is not a ::marker pseudo-element \
(pseudo={:?}, anonymous_type={:?})",
marker_index, marker_node.pseudo_element, marker_node.anonymous_type
)));
}
if marker_node.anonymous_type != Some(AnonymousBoxType::ListItemMarker) {
return String::new();
}
}
let list_item_index = match marker_node.parent {
Some(p) => p,
None => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::error(
"[generate_list_marker_text] ERROR: Marker has no parent".to_string(),
));
}
return String::new();
}
};
let list_item_node = match tree.get(list_item_index) {
Some(n) => n,
None => return String::new(),
};
let list_item_dom_id = match list_item_node.dom_node_id {
Some(id) => id,
None => {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::error(
"[generate_list_marker_text] ERROR: List-item has no DOM ID".to_string(),
));
}
return String::new();
}
};
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[generate_list_marker_text] marker_index={}, list_item_index={}, \
list_item_dom_id={:?}",
marker_index, list_item_index, list_item_dom_id
)));
}
let list_container_dom_id = if let Some(grandparent_index) = list_item_node.parent {
if let Some(grandparent) = tree.get(grandparent_index) {
grandparent.dom_node_id
} else {
None
}
} else {
None
};
let list_style_type = if let Some(container_id) = list_container_dom_id {
let container_type = get_list_style_type(styled_dom, Some(container_id));
if container_type != StyleListStyleType::default() {
container_type
} else {
get_list_style_type(styled_dom, Some(list_item_dom_id))
}
} else {
get_list_style_type(styled_dom, Some(list_item_dom_id))
};
let counter_value = counters
.get(&(list_item_index, "list-item".to_string()))
.copied()
.unwrap_or_else(|| {
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::warning(format!(
"[generate_list_marker_text] WARNING: No counter found for list-item at index \
{}, defaulting to 1",
list_item_index
)));
}
1
});
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[generate_list_marker_text] counter_value={} for list_item_index={}",
counter_value, list_item_index
)));
}
let marker_text = format_counter(counter_value, list_style_type);
if matches!(
list_style_type,
StyleListStyleType::Decimal
| StyleListStyleType::DecimalLeadingZero
| StyleListStyleType::LowerAlpha
| StyleListStyleType::UpperAlpha
| StyleListStyleType::LowerRoman
| StyleListStyleType::UpperRoman
| StyleListStyleType::LowerGreek
| StyleListStyleType::UpperGreek
) {
format!("{}. ", marker_text)
} else {
format!("{} ", marker_text)
}
}
fn generate_list_marker_segments(
tree: &LayoutTree,
styled_dom: &StyledDom,
marker_index: usize,
counters: &BTreeMap<(usize, String), i32>,
base_style: Arc<StyleProperties>,
debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
) -> Vec<StyledRun> {
let marker_text =
generate_list_marker_text(tree, styled_dom, marker_index, counters, debug_messages);
if marker_text.is_empty() {
return Vec::new();
}
if let Some(msgs) = debug_messages {
let font_families: Vec<&str> = match &base_style.font_stack {
crate::text3::cache::FontStack::Stack(selectors) => {
selectors.iter().map(|f| f.family.as_str()).collect()
}
crate::text3::cache::FontStack::Ref(_) => vec!["<embedded-font>"],
};
msgs.push(LayoutDebugMessage::info(format!(
"[generate_list_marker_segments] Marker text: '{}' with font stack: {:?}",
marker_text,
font_families
)));
}
vec![StyledRun {
text: marker_text,
style: base_style,
logical_start_byte: 0,
source_node_id: None,
}]
}
pub(crate) fn split_text_for_whitespace(
styled_dom: &StyledDom,
dom_id: NodeId,
text: &str,
style: Arc<StyleProperties>,
) -> Vec<InlineContent> {
use crate::text3::cache::{BreakType, ClearType, InlineBreak};
let node_hierarchy = styled_dom.node_hierarchy.as_container();
let parent_id = node_hierarchy[dom_id].parent_id();
let white_space = if let Some(parent) = parent_id {
let styled_nodes = styled_dom.styled_nodes.as_container();
let parent_state = styled_nodes
.get(parent)
.map(|n| n.styled_node_state.clone())
.unwrap_or_default();
match get_white_space_property(styled_dom, parent, &parent_state) {
MultiValue::Exact(ws) => ws,
_ => StyleWhiteSpace::Normal,
}
} else {
StyleWhiteSpace::Normal
};
let mut result = Vec::new();
match white_space {
StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap | StyleWhiteSpace::BreakSpaces => {
let mut lines = text.split('\n').peekable();
let mut content_index = 0;
while let Some(line) = lines.next() {
let mut tab_parts = line.split('\t').peekable();
while let Some(part) = tab_parts.next() {
if !part.is_empty() {
result.push(InlineContent::Text(StyledRun {
text: part.to_string(),
style: Arc::clone(&style),
logical_start_byte: 0,
source_node_id: Some(dom_id),
}));
}
if tab_parts.peek().is_some() {
result.push(InlineContent::Tab { style: Arc::clone(&style) });
}
}
if lines.peek().is_some() {
result.push(InlineContent::LineBreak(InlineBreak {
break_type: BreakType::Hard,
clear: ClearType::None,
content_index,
}));
content_index += 1;
}
}
}
StyleWhiteSpace::PreLine => {
let mut lines = text.split('\n').peekable();
let mut content_index = 0;
while let Some(line) = lines.next() {
let collapsed: String = line.split_whitespace().collect::<Vec<_>>().join(" ");
if !collapsed.is_empty() {
result.push(InlineContent::Text(StyledRun {
text: collapsed,
style: Arc::clone(&style),
logical_start_byte: 0,
source_node_id: Some(dom_id),
}));
}
if lines.peek().is_some() {
result.push(InlineContent::LineBreak(InlineBreak {
break_type: BreakType::Hard,
clear: ClearType::None,
content_index,
}));
content_index += 1;
}
}
}
StyleWhiteSpace::Normal | StyleWhiteSpace::Nowrap => {
let collapsed: String = text
.chars()
.map(|c| if c.is_whitespace() { ' ' } else { c })
.collect::<String>()
.split(' ')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
let final_text = if collapsed.is_empty() && !text.is_empty() {
" ".to_string()
} else if !collapsed.is_empty() {
let had_leading = text.chars().next().map(|c| c.is_whitespace()).unwrap_or(false);
let had_trailing = text.chars().last().map(|c| c.is_whitespace()).unwrap_or(false);
let mut result = String::new();
if had_leading { result.push(' '); }
result.push_str(&collapsed);
if had_trailing && !had_leading { result.push(' '); }
else if had_trailing && had_leading && collapsed.is_empty() { }
else if had_trailing { result.push(' '); }
result
} else {
collapsed
};
if !final_text.is_empty() {
result.push(InlineContent::Text(StyledRun {
text: final_text,
style,
logical_start_byte: 0,
source_node_id: Some(dom_id),
}));
}
}
}
result
}