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, ColumnWidth, LayoutBorderSpacing, LayoutClear, LayoutDisplay, LayoutFloat,
LayoutHeight, LayoutJustifyContent, LayoutOverflow, LayoutPosition, LayoutTableLayout,
LayoutTextJustify, LayoutWidth, LayoutWritingMode, ShapeInside, ShapeOutside,
StyleBorderCollapse, StyleCaptionSide, StyleEmptyCells,
},
property::CssProperty,
style::{
BorderStyle, StyleDirection, StyleHyphens, StyleLineBreak, StyleListStylePosition,
StyleListStyleType, StyleOverflowWrap, StyleTextAlign, StyleTextAlignLast,
StyleTextBoxTrim, StyleTextCombineUpright, StyleTextOrientation, StyleUnicodeBidi,
StyleVerticalAlign, StyleVisibility, StyleWhiteSpace, StyleWordBreak,
},
},
};
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_border_bottom_width, get_css_border_top_width,
get_css_height, get_css_padding_bottom, get_css_padding_top,
get_css_width, get_direction_property, get_unicode_bidi_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_text_box_trim_property, get_text_orientation_property,
get_vertical_align_property, get_visibility, get_white_space_property,
get_writing_mode, MultiValue,
},
layout_tree::{
AnonymousBoxType, CachedInlineLayout, LayoutNode, LayoutNodeHot, LayoutNodeWarm, LayoutNodeCold, 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 writing_mode_ctx: super::geometry::WritingModeContext,
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) - float.margin.main_start(wm);
let float_main_end = float_main_start + float.rect.size.main(wm)
+ float.margin.main_start(wm) + float.margin.main_end(wm);
if main_end > float_main_start && main_start < float_main_end {
let float_cross_start = float.rect.origin.cross(wm) - float.margin.cross_start(wm);
let float_cross_end = float_cross_start + float.rect.size.cross(wm)
+ float.margin.cross_start(wm) + float.margin.cross_end(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 HashMap<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 = HashMap::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)
}
FormattingContext::TableCell | FormattingContext::TableCaption => {
let mut temp_float_cache = HashMap::new();
layout_bfc(ctx, tree, text_cache, node_index, constraints, &mut temp_float_cache)
}
_ => {
let mut temp_float_cache = HashMap::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 bp = node.box_props.unpack();
let width_adjustment = bp.border.left
+ bp.border.right
+ bp.padding.left
+ bp.padding.right;
let height_adjustment = bp.border.top
+ bp.border.bottom
+ bp.padding.top
+ bp.padding.bottom;
let root_border_box = node.used_size;
let effective_width = if has_explicit_width {
explicit_width
} else if is_root {
root_border_box.as_ref().map(|s| s.width).or_else(|| {
if constraints.available_size.width.is_finite() {
Some(constraints.available_size.width + width_adjustment)
} else {
None
}
})
} else {
None
};
let effective_height = if has_explicit_height {
explicit_height
} else if is_root {
root_border_box.as_ref().map(|s| s.height).or_else(|| {
if constraints.available_size.height.is_finite() {
Some(constraints.available_size.height + height_adjustment)
} else {
None
}
})
} else {
None
};
let has_effective_width = effective_width.is_some();
let has_effective_height = effective_height.is_some();
let adjusted_width = if has_explicit_width && !is_root {
explicit_width.map(|w| w + width_adjustment)
} else if has_explicit_width && is_root {
explicit_width
} else {
effective_width
};
let adjusted_height = if has_explicit_height && !is_root {
explicit_height.map(|h| h + height_adjustment)
} else if has_explicit_height && is_root {
explicit_height
} 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
);
if is_root {
if let (Some(aw), Some(ah)) = (adjusted_width, adjusted_height) {
if let Some(node_mut) = tree.get_mut(node_index) {
node_mut.used_size = Some(LogicalSize::new(aw, ah));
}
}
}
let border_left = bp.border.left;
let border_top = bp.border.top;
let taffy_output =
taffy_bridge::layout_taffy_subtree(ctx, tree, text_cache, node_index, taffy_inputs);
let mut output = LayoutOutput::default();
let raw = translate_taffy_size_back(taffy_output.content_size);
output.overflow_size = LogicalSize::new(
(raw.width - border_left).max(0.0),
(raw.height - border_top).max(0.0),
);
let children: Vec<usize> = tree.children(node_index).to_vec();
for &child_idx in &children {
if let Some(warm_node) = tree.warm(child_idx) {
if let Some(pos) = warm_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: &LayoutNodeHot,
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 | LayoutWidth::FitContent(_) => (None, false),
LayoutWidth::Calc(items) => {
let node_state = &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state;
let em = get_element_font_size(ctx.styled_dom, id, node_state);
let calc_ctx = super::calc::CalcResolveContext {
items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
};
let px = super::calc::evaluate_calc(&calc_ctx, constraints.available_size.width);
(Some(px), true)
}
}
})
.unwrap_or((None, false))
}
fn resolve_explicit_dimension_height<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
node: &LayoutNodeHot,
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 | LayoutHeight::FitContent(_) => (None, false),
LayoutHeight::Calc(items) => {
let node_state = &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state;
let em = get_element_font_size(ctx.styled_dom, id, node_state);
let calc_ctx = super::calc::CalcResolveContext {
items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
};
let px = super::calc::evaluate_calc(&calc_ctx, constraints.available_size.height);
(Some(px), true)
}
}
})
.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) - f.margin.main_start(wm);
let f_main_end = f_main_start + f.rect.size.main(wm)
+ f.margin.main_start(wm) + f.margin.main_end(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) + f.margin.main_end(wm))
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
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 HashMap<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,
tree.children(node_index).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 {
let inner = node.box_props.inner_size(used_size, writing_mode);
let height_is_auto = tree
.warm(node_index)
.map(|w| w.computed_style.height.is_none())
.unwrap_or(true);
if height_is_auto {
LogicalSize::new(inner.width, constraints.available_size.height)
} else {
inner
}
} 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 => {
crate::solver3::getters::get_layout_scrollbar_width_px(ctx, dom_id, &styled_node_state)
}
LayoutOverflow::Auto => {
let already_needs = tree.warm(node_index)
.and_then(|w| w.scrollbar_info.as_ref())
.map(|s| s.needs_vertical)
.unwrap_or(false);
if already_needs {
crate::solver3::getters::get_layout_scrollbar_width_px(ctx, dom_id, &styled_node_state)
} else {
0.0
}
}
_ => 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;
let bfc_children = tree.children(node_index).to_vec();
for &child_index in &bfc_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 node_bp = node.box_props.unpack();
let parent_margin_top = node_bp.margin.main_start(writing_mode);
let parent_margin_bottom = node_bp.margin.main_end(writing_mode);
let establishes_own_bfc = establishes_new_bfc(ctx, &node, tree.cold(node_index));
let is_bfc_root = node.parent.is_none() || establishes_own_bfc;
let parent_has_top_blocker = establishes_own_bfc
|| has_margin_collapse_blocker(&node_bp, writing_mode, true);
let parent_has_bottom_blocker = establishes_own_bfc
|| has_margin_collapse_blocker(&node_bp, 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;
let pos_children = tree.children(node_index).to_vec();
for &child_index in &pos_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 = tree.warm(child_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
let child_bp = child_node.box_props.unpack();
let computed_size = crate::solver3::sizing::calculate_used_size_for_node(
ctx.styled_dom,
child_dom_id,
children_containing_block_size,
intrinsic,
&child_bp,
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 child_bp2 = child_node.box_props.unpack();
let float_margin = &child_bp2.margin;
let float_clear = get_clear_property(ctx.styled_dom, Some(node_id));
let float_y = if float_clear != LayoutClear::None {
float_context.clearance_offset(float_clear, main_pen + last_margin_bottom, writing_mode)
} else {
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 = tree.warm(child_index).and_then(|w| w.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.unpack(),
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_bp = child_node.box_props.unpack();
let child_margin = &child_bp.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_own_margin_top = child_margin.main_start(writing_mode);
let child_own_margin_bottom = child_margin.main_end(writing_mode);
let child_escaped_top = if !has_margin_collapse_blocker(&child_bp, writing_mode, true) {
tree.warm(child_index).and_then(|w| w.escaped_top_margin)
} else { None };
let child_escaped_bottom = if !has_margin_collapse_blocker(&child_bp, writing_mode, false) {
tree.warm(child_index).and_then(|w| w.escaped_bottom_margin)
} else { None };
let child_margin_top = child_escaped_top.unwrap_or(child_own_margin_top);
let child_margin_bottom = child_escaped_bottom.unwrap_or(child_own_margin_bottom);
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_bp, writing_mode, true);
let child_has_bottom_blocker =
has_margin_collapse_blocker(&child_bp, 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(tree, child_index);
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 hypothetical = main_pen + collapse_margins(last_margin_bottom, child_margin_top);
let cleared_position =
float_context.clearance_offset(child_clear, hypothetical, writing_mode);
debug_info!(
ctx,
"[layout_bfc] Child {} clearance check: cleared_position={}, hypothetical={} (main_pen={} + collapse({}, {}))",
child_index,
cleared_position,
hypothetical,
main_pen,
last_margin_bottom,
child_margin_top
);
if cleared_position > hypothetical {
debug_info!(
ctx,
"[layout_bfc] Applying clearance: child={}, clear={:?}, old_pen={}, new_pen={}",
child_index,
child_clear,
main_pen,
cleared_position
);
main_pen = cleared_position;
true } else {
false
}
} else {
false
};
if is_first_child {
is_first_child = false;
if clearance_applied {
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 {
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 avoids_floats = establishes_new_bfc(ctx, child_node, tree.cold(child_index))
|| is_block_level_replaced(ctx, child_node);
let (cross_start, cross_end, available_cross) = if avoids_floats {
let child_cross_needed = child_size.cross(writing_mode);
let bfc_cross = constraints.available_size.cross(writing_mode);
let (mut start, mut end) = float_context.available_line_box_space(
main_pen,
main_pen + child_size.main(writing_mode),
bfc_cross,
writing_mode,
);
let mut available = end - start;
if available < child_cross_needed && !float_context.floats.is_empty() {
let clear_to = float_context.floats.iter()
.filter(|f| {
let f_main_start = f.rect.origin.main(writing_mode) - f.margin.main_start(writing_mode);
let f_main_end = f_main_start + f.rect.size.main(writing_mode)
+ f.margin.main_start(writing_mode) + f.margin.main_end(writing_mode);
f_main_end > main_pen && f_main_start < main_pen + child_size.main(writing_mode)
})
.map(|f| {
f.rect.origin.main(writing_mode) + f.rect.size.main(writing_mode)
+ f.margin.main_end(writing_mode)
})
.fold(main_pen, f32::max);
if clear_to > main_pen {
main_pen = clear_to;
let (s, e) = float_context.available_line_box_space(
main_pen,
main_pen + child_size.main(writing_mode),
bfc_cross,
writing_mode,
);
start = s;
end = e;
available = end - start;
}
}
debug_info!(
ctx,
"[layout_bfc] Child {} avoids floats: 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)?;
let cbp = child_node.box_props.unpack();
(
cbp.margin.clone(),
cbp.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 avoids_floats {
let cross_pos = if child_margin_auto.left && child_margin_auto.right {
let remaining = (available_cross - child_used_size.cross(writing_mode)).max(0.0);
debug_info!(
ctx,
"[layout_bfc] Child {} BFC + margin:auto centering: available={}, size={}, offset={}",
child_index, available_cross, child_used_size.cross(writing_mode), remaining / 2.0
);
cross_start + remaining / 2.0
} else if child_margin_auto.left {
let remaining = (available_cross - child_used_size.cross(writing_mode) - child_margin.right).max(0.0);
cross_start + remaining
} else {
cross_start + child_margin.cross_start(writing_mode)
};
(cross_pos, 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 {
let is_rtl = tree.get(node_index)
.and_then(|n| n.dom_node_id)
.map_or(false, |cb_dom_id| {
let node_state = ctx.styled_dom.styled_nodes.as_container()
.get(cb_dom_id)
.map(|s| s.styled_node_state.clone())
.unwrap_or_default();
matches!(
get_direction_property(ctx.styled_dom, cb_dom_id, &node_state),
MultiValue::Exact(StyleDirection::Rtl)
)
});
let cross_pos = if is_rtl {
available_cross - child_cross_size - child_margin.cross_end(writing_mode)
} else {
child_margin.cross_start(writing_mode)
};
debug_info!(
ctx,
"[layout_bfc] Child {} NO auto margins (over-constrained), is_rtl={}, cross_pos={}",
child_index,
is_rtl,
cross_pos
);
cross_pos
};
(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={}, avoids_floats={}",
child_index,
final_pos,
main_pen,
avoids_floats
);
if is_inline_fc && !avoids_floats {
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 cbp = child_node.box_props.unpack();
let padding_border_cross = cbp.padding.cross_start(writing_mode)
+ cbp.border.cross_start(writing_mode);
let padding_border_main = cbp.padding.main_start(writing_mode)
+ cbp.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,
writing_mode_ctx: constraints.writing_mode_ctx,
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;
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 {
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_child_bp = last_child.box_props.unpack();
let last_has_bottom_blocker =
has_margin_collapse_blocker(&last_child_bp, 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 mut content_box_height = main_pen - total_escaped_top_margin
- escaped_bottom_margin.unwrap_or(0.0);
if is_bfc_root {
for float_box in &float_context.floats {
let float_bottom_margin_edge = float_box.rect.origin.main(writing_mode)
+ float_box.rect.size.main(writing_mode)
+ float_box.margin.main_end(writing_mode);
if float_bottom_margin_edge > content_box_height {
content_box_height = float_bottom_margin_edge;
}
}
}
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(warm_mut) = tree.warm_mut(node_index) {
warm_mut.escaped_top_margin = escaped_top_margin;
warm_mut.escaped_bottom_margin = escaped_bottom_margin;
}
if let Some(warm_mut) = tree.warm_mut(node_index) {
warm_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 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 {
tree.children(node_index)
.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
.warm(node_index)
.and_then(|n| n.inline_layout_result.as_ref());
if let Some(cached) = cached_ifc {
if let Some(ref line_breaks) = cached.line_breaks {
let old_advances: Vec<f32> = cached.item_metrics.iter()
.map(|m| m.advance_width)
.collect();
let result = crate::text3::cache::try_incremental_relayout(
&[], &old_advances,
&old_advances, line_breaks,
);
match result {
crate::text3::cache::IncrementalRelayoutResult::GlyphSwap => {
debug_info!(ctx, "[layout_ifc] Phase 2d: GlyphSwap — reusing cached layout");
let main_frag = &cached.layout;
let frag_bounds = main_frag.bounds();
let mut output = LayoutOutput::default();
output.overflow_size = LogicalSize::new(frag_bounds.width, frag_bounds.height);
output.baseline = main_frag.last_baseline();
for positioned_item in &main_frag.items {
if let ShapedItem::Object { source, .. } = &positioned_item.item {
if let Some(&child_node_index) = child_map.get(source) {
output.positions.insert(child_node_index, LogicalPosition {
x: positioned_item.position.x,
y: positioned_item.position.y,
});
}
}
}
return Ok(output);
}
_ => {
}
}
}
}
}
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();
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 warm_node = tree.warm_mut(node_index).ok_or(LayoutError::InvalidTree)?;
let should_store = match &warm_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 {
warm_node.inline_layout_result = Some(CachedInlineLayout::new_with_constraints(
main_frag.clone(),
current_width_type,
has_floats,
cached_constraints.clone(),
));
}
output.overflow_size = LogicalSize::new(frag_bounds.width, frag_bounds.height);
output.baseline = main_frag.last_baseline();
warm_node.baseline = output.baseline;
let ifc_node_state = &ctx.styled_dom.styled_nodes.as_container()[ifc_root_dom_id].styled_node_state;
let text_box_trim = {
let skip = ctx.styled_dom
.css_property_cache
.ptr
.compact_cache
.as_ref()
.map(|cc| cc.dom_declared_flags & azul_css::compact_cache::DOM_HAS_TEXT_BOX_TRIM == 0)
.unwrap_or(false);
if skip {
StyleTextBoxTrim::None
} else {
get_text_box_trim_property(ctx.styled_dom, ifc_root_dom_id, ifc_node_state)
.unwrap_or(StyleTextBoxTrim::None)
}
};
if text_box_trim != StyleTextBoxTrim::None && !main_frag.items.is_empty() {
let half_leading = (cached_constraints.resolved_line_height()
- (cached_constraints.strut_ascent + cached_constraints.strut_descent))
/ 2.0;
let half_leading = half_leading.max(0.0);
let has_pad_or_border_top = match get_css_padding_top(ctx.styled_dom, ifc_root_dom_id, ifc_node_state) {
MultiValue::Exact(pv) => pv.number.get() != 0.0,
_ => false,
} || match get_css_border_top_width(ctx.styled_dom, ifc_root_dom_id, ifc_node_state) {
MultiValue::Exact(pv) => pv.number.get() != 0.0,
_ => false,
};
let has_pad_or_border_bottom = match get_css_padding_bottom(ctx.styled_dom, ifc_root_dom_id, ifc_node_state) {
MultiValue::Exact(pv) => pv.number.get() != 0.0,
_ => false,
} || match get_css_border_bottom_width(ctx.styled_dom, ifc_root_dom_id, ifc_node_state) {
MultiValue::Exact(pv) => pv.number.get() != 0.0,
_ => false,
};
let trim_start = matches!(text_box_trim, StyleTextBoxTrim::TrimStart | StyleTextBoxTrim::TrimBoth)
&& !has_pad_or_border_top;
let trim_end = matches!(text_box_trim, StyleTextBoxTrim::TrimEnd | StyleTextBoxTrim::TrimBoth)
&& !has_pad_or_border_bottom;
let mut height_reduction = 0.0;
if trim_start && half_leading > 0.0 {
height_reduction += half_leading;
}
if trim_end && half_leading > 0.0 {
height_reduction += half_leading;
}
if height_reduction > 0.0 {
output.overflow_size.height = (output.overflow_size.height - height_reduction).max(0.0);
}
}
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 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 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: &LayoutNodeHot, cold: Option<&LayoutNodeCold>) -> bool {
if cold.and_then(|c| c.anonymous_type) == Some(AnonymousBoxType::TableWrapper) {
return true;
}
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;
}
{
let hierarchy = ctx.styled_dom.node_hierarchy.as_container();
if let Some(parent_dom_id) = hierarchy[dom_id].parent_id() {
let parent_state = &ctx.styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
let child_wm = get_writing_mode(ctx.styled_dom, dom_id, node_state).unwrap_or_default();
let parent_wm = get_writing_mode(ctx.styled_dom, parent_dom_id, parent_state).unwrap_or_default();
if child_wm != parent_wm {
return true;
}
}
}
false
}
fn is_block_level_replaced<T: ParsedFontTrait>(ctx: &LayoutContext<'_, T>, node: &LayoutNodeHot) -> bool {
let Some(dom_id) = node.dom_node_id else {
return false;
};
let display = get_display_property(ctx.styled_dom, Some(dom_id));
let is_block_level = matches!(
display,
MultiValue::Exact(LayoutDisplay::Block | LayoutDisplay::ListItem | LayoutDisplay::FlowRoot)
);
if !is_block_level {
return false;
}
let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
matches!(
node_data.get_node_type(),
NodeType::Image(_)
)
}
fn translate_to_text3_constraints<'a, T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
constraints: &'a LayoutConstraints<'a>,
styled_dom: &StyledDom,
dom_id: NodeId,
) -> UnifiedConstraints {
use azul_css::compact_cache::{
DOM_HAS_SHAPE_INSIDE, DOM_HAS_SHAPE_OUTSIDE, DOM_HAS_TEXT_JUSTIFY,
DOM_HAS_TEXT_INDENT, DOM_HAS_COLUMN_COUNT, DOM_HAS_COLUMN_GAP,
DOM_HAS_COLUMN_WIDTH,
DOM_HAS_INITIAL_LETTER, DOM_HAS_INITIAL_LETTER_ALIGN,
DOM_HAS_LINE_CLAMP, DOM_HAS_HANGING_PUNCTUATION,
DOM_HAS_TEXT_COMBINE_UPRIGHT, DOM_HAS_EXCLUSION_MARGIN,
DOM_HAS_SHAPE_MARGIN,
DOM_HAS_HYPHENATION_LANGUAGE, DOM_HAS_UNICODE_BIDI,
DOM_HAS_HYPHENS, DOM_HAS_WORD_BREAK, DOM_HAS_OVERFLOW_WRAP,
DOM_HAS_LINE_BREAK, DOM_HAS_TEXT_ALIGN_LAST, DOM_HAS_LINE_HEIGHT,
};
let dom_declared = styled_dom
.css_property_cache
.ptr
.compact_cache
.as_ref()
.map(|cc| cc.dom_declared_flags)
.unwrap_or(!0u32);
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 = if dom_declared & DOM_HAS_SHAPE_INSIDE != 0 {
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()
} else {
Vec::new()
};
debug_info!(
ctx,
"Final shape_boundaries count: {}",
shape_boundaries.len()
);
debug_info!(ctx, "Checking shape-outside for node {:?}", id);
if dom_declared & DOM_HAS_SHAPE_OUTSIDE != 0 {
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 = if dom_declared & DOM_HAS_TEXT_JUSTIFY != 0 {
styled_dom
.css_property_cache
.ptr
.get_text_justify(node_data, &id, node_state)
.and_then(|s| s.get_property().copied())
.unwrap_or_default()
} else {
Default::default()
};
let font_size = get_element_font_size(styled_dom, id, node_state);
let line_height_value = if dom_declared & DOM_HAS_LINE_HEIGHT != 0 {
styled_dom
.css_property_cache
.ptr
.get_line_height(node_data, &id, node_state)
.and_then(|s| s.get_property().cloned())
.unwrap_or_default()
} else {
Default::default()
};
let hyphenation = if dom_declared & DOM_HAS_HYPHENS != 0 {
styled_dom
.css_property_cache
.ptr
.get_hyphens(node_data, &id, node_state)
.and_then(|s| s.get_property().copied())
.unwrap_or_default()
} else {
Default::default()
};
let word_break_css = if dom_declared & DOM_HAS_WORD_BREAK != 0 {
styled_dom
.css_property_cache
.ptr
.get_word_break(node_data, &id, node_state)
.and_then(|s| s.get_property().copied())
.unwrap_or_default()
} else {
Default::default()
};
let overflow_wrap_css = if dom_declared & DOM_HAS_OVERFLOW_WRAP != 0 {
styled_dom
.css_property_cache
.ptr
.get_overflow_wrap(node_data, &id, node_state)
.and_then(|s| s.get_property().copied())
.unwrap_or_default()
} else {
Default::default()
};
let line_break_css = if dom_declared & DOM_HAS_LINE_BREAK != 0 {
styled_dom
.css_property_cache
.ptr
.get_line_break(node_data, &id, node_state)
.and_then(|s| s.get_property().copied())
.unwrap_or_default()
} else {
Default::default()
};
let text_align_last_css = if dom_declared & DOM_HAS_TEXT_ALIGN_LAST != 0 {
styled_dom
.css_property_cache
.ptr
.get_text_align_last(node_data, &id, node_state)
.and_then(|s| s.get_property().copied())
.unwrap_or_default()
} else {
Default::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,
StyleVerticalAlign::Percentage(p) => {
let lh_n = line_height_value.inner.normalized();
let resolved_lh = if lh_n < 0.0 { -lh_n } else { lh_n * font_size };
let offset = p.normalized() * resolved_lh;
text3::cache::VerticalAlign::Offset(offset)
}
StyleVerticalAlign::Length(l) => {
let offset = super::calc::resolve_pixel_value(&l, 0.0, font_size, font_size);
text3::cache::VerticalAlign::Offset(offset)
}
};
let text_orientation = match get_text_orientation_property(styled_dom, id, node_state) {
MultiValue::Exact(o) => match o {
StyleTextOrientation::Mixed => text3::cache::TextOrientation::Mixed,
StyleTextOrientation::Upright => text3::cache::TextOrientation::Upright,
StyleTextOrientation::Sideways => text3::cache::TextOrientation::Sideways,
},
_ => text3::cache::TextOrientation::default(),
};
let direction = match constraints.writing_mode {
LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr
if matches!(text_orientation, text3::cache::TextOrientation::Upright) =>
{
Some(text3::cache::BidiDirection::Ltr)
}
_ => 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,
},
};
let unicode_bidi_val = if dom_declared & DOM_HAS_UNICODE_BIDI != 0 {
match get_unicode_bidi_property(styled_dom, id, node_state) {
MultiValue::Exact(u) => match u {
StyleUnicodeBidi::Normal => text3::cache::UnicodeBidi::Normal,
StyleUnicodeBidi::Embed => text3::cache::UnicodeBidi::Embed,
StyleUnicodeBidi::Isolate => text3::cache::UnicodeBidi::Isolate,
StyleUnicodeBidi::BidiOverride => text3::cache::UnicodeBidi::BidiOverride,
StyleUnicodeBidi::IsolateOverride => text3::cache::UnicodeBidi::IsolateOverride,
StyleUnicodeBidi::Plaintext => text3::cache::UnicodeBidi::Plaintext,
},
_ => text3::cache::UnicodeBidi::Normal,
}
} else {
text3::cache::UnicodeBidi::Normal
};
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_prop = if dom_declared & DOM_HAS_TEXT_INDENT != 0 {
styled_dom
.css_property_cache
.ptr
.get_text_indent(node_data, &id, node_state)
.and_then(|s| s.get_property().cloned())
} else {
None
};
let is_intrinsic_sizing = matches!(
constraints.available_width_type,
Text3AvailableSpace::MinContent | Text3AvailableSpace::MaxContent
);
let text_indent = text_indent_prop
.map(|ti| {
if is_intrinsic_sizing && ti.inner.to_percent().is_some() {
return 0.0;
}
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(ctx.viewport_size.width, ctx.viewport_size.height),
};
ti.inner
.resolve_with_context(&context, PropertyContext::Other)
})
.unwrap_or(0.0);
let text_indent_each_line = text_indent_prop.map(|ti| ti.each_line).unwrap_or(false);
let text_indent_hanging = text_indent_prop.map(|ti| ti.hanging).unwrap_or(false);
let column_resolve_ctx = 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(ctx.viewport_size.width, ctx.viewport_size.height),
};
macro_rules! declared_prop {
($bit:expr, $getter:ident) => {
if dom_declared & $bit != 0 {
styled_dom
.css_property_cache
.ptr
.$getter(node_data, &id, node_state)
.and_then(|s| s.get_property())
} else {
None
}
};
}
let column_gap = declared_prop!(DOM_HAS_COLUMN_GAP, get_column_gap)
.map(|cg| {
cg.inner
.resolve_with_context(&column_resolve_ctx, PropertyContext::Other)
})
.unwrap_or_else(|| get_element_font_size(styled_dom, id, node_state));
let column_width =
declared_prop!(DOM_HAS_COLUMN_WIDTH, get_column_width).and_then(|cw| match cw {
ColumnWidth::Auto => None,
ColumnWidth::Length(px) => {
Some(px.resolve_with_context(&column_resolve_ctx, PropertyContext::Other))
}
});
let explicit_column_count =
declared_prop!(DOM_HAS_COLUMN_COUNT, get_column_count).copied();
let columns = match (explicit_column_count, column_width) {
(Some(ColumnCount::Integer(n)), _) => n,
(_, Some(cw)) if cw > 0.0 => {
let avail = constraints.available_size.width;
((avail + column_gap) / (cw + column_gap)).floor().max(1.0) as u32
}
_ => 1,
};
let resolved_ws = match get_white_space_property(styled_dom, id, node_state) {
MultiValue::Exact(ws) => ws,
_ => StyleWhiteSpace::Normal,
};
let text_wrap = match resolved_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,
};
let white_space_mode = match resolved_ws {
StyleWhiteSpace::Normal => text3::cache::WhiteSpaceMode::Normal,
StyleWhiteSpace::Nowrap => text3::cache::WhiteSpaceMode::Nowrap,
StyleWhiteSpace::Pre => text3::cache::WhiteSpaceMode::Pre,
StyleWhiteSpace::PreWrap => text3::cache::WhiteSpaceMode::PreWrap,
StyleWhiteSpace::PreLine => text3::cache::WhiteSpaceMode::PreLine,
StyleWhiteSpace::BreakSpaces => text3::cache::WhiteSpaceMode::BreakSpaces,
};
let initial_letter_align = if dom_declared & DOM_HAS_INITIAL_LETTER_ALIGN != 0 {
styled_dom
.css_property_cache
.ptr
.get_initial_letter_align(node_data, &id, node_state)
.and_then(|s| s.get_property())
.map(|a| match a {
azul_css::props::style::text::StyleInitialLetterAlign::Auto => text3::cache::InitialLetterAlign::Auto,
azul_css::props::style::text::StyleInitialLetterAlign::Alphabetic => text3::cache::InitialLetterAlign::Alphabetic,
azul_css::props::style::text::StyleInitialLetterAlign::Hanging => text3::cache::InitialLetterAlign::Hanging,
azul_css::props::style::text::StyleInitialLetterAlign::Ideographic => text3::cache::InitialLetterAlign::Ideographic,
})
.unwrap_or(text3::cache::InitialLetterAlign::Auto)
} else {
text3::cache::InitialLetterAlign::Auto
};
let initial_letter = if dom_declared & DOM_HAS_INITIAL_LETTER != 0 {
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(),
align: initial_letter_align,
}
})
} else {
None
};
if let Some(ref il) = initial_letter {
let lh_n = line_height_value.inner.normalized();
let computed_line_height = if lh_n < 0.0 { -lh_n } else { lh_n * font_size };
let (letter_w, letter_h) = layout_initial_letter(
il.size,
il.sink,
constraints.available_size.width,
computed_line_height,
);
if letter_w > 0.0 && letter_h > 0.0 {
shape_exclusions.push(ShapeBoundary::Rectangle(crate::text3::cache::Rect {
x: 0.0,
y: 0.0,
width: letter_w,
height: letter_h,
}));
}
}
let line_clamp = if dom_declared & DOM_HAS_LINE_CLAMP != 0 {
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))
} else {
None
};
let hanging_punctuation = if dom_declared & DOM_HAS_HANGING_PUNCTUATION != 0 {
styled_dom
.css_property_cache
.ptr
.get_hanging_punctuation(node_data, &id, node_state)
.and_then(|s| s.get_property())
.map(|hp| hp.is_enabled())
.unwrap_or(false)
} else {
false
};
let text_combine_upright = if dom_declared & DOM_HAS_TEXT_COMBINE_UPRIGHT != 0 {
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),
})
} else {
None
};
let exclusion_margin_base = if dom_declared & DOM_HAS_EXCLUSION_MARGIN != 0 {
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)
} else {
0.0
};
let shape_margin = if dom_declared & DOM_HAS_SHAPE_MARGIN != 0 {
styled_dom
.css_property_cache
.ptr
.get_shape_margin(node_data, &id, node_state)
.and_then(|s| s.get_property())
.map(|sm| sm.inner.number.get() as f32)
.unwrap_or(0.0)
} else {
0.0
};
let exclusion_margin = exclusion_margin_base + shape_margin;
let hyphenation_language = if dom_declared & DOM_HAS_HYPHENATION_LANGUAGE != 0 {
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>
}
})
} else {
None
};
UnifiedConstraints {
exclusion_margin,
hyphenation_language,
text_indent,
text_indent_each_line,
text_indent_hanging,
initial_letter,
line_clamp,
columns,
column_gap,
hanging_punctuation,
text_wrap,
white_space_mode,
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, unicode_bidi: unicode_bidi_val,
hyphenation: match hyphenation {
StyleHyphens::None => text3::cache::Hyphens::None,
StyleHyphens::Manual => text3::cache::Hyphens::Manual,
StyleHyphens::Auto => text3::cache::Hyphens::Auto,
},
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::InterWord,
LayoutTextJustify::InterWord => text3::cache::JustifyContent::InterWord,
LayoutTextJustify::InterCharacter => text3::cache::JustifyContent::InterCharacter,
LayoutTextJustify::Distribute => text3::cache::JustifyContent::InterCharacter, },
line_height: text3::cache::LineHeight::Px({
let n = line_height_value.inner.normalized();
if n < 0.0 { -n } else { n * font_size }
}),
strut_ascent: font_size * 0.8,
strut_descent: font_size * 0.2,
strut_x_height: font_size * 0.5, ch_width: font_size * 0.5, vertical_align,
overflow_wrap: if word_break_css == StyleWordBreak::BreakWord {
text3::cache::OverflowWrap::Anywhere
} else {
match overflow_wrap_css {
StyleOverflowWrap::Normal => text3::cache::OverflowWrap::Normal,
StyleOverflowWrap::Anywhere => text3::cache::OverflowWrap::Anywhere,
StyleOverflowWrap::BreakWord => text3::cache::OverflowWrap::BreakWord,
}
},
text_align_last: match text_align_last_css {
StyleTextAlignLast::Auto => text3::cache::TextAlign::default(),
StyleTextAlignLast::Start => text3::cache::TextAlign::Start,
StyleTextAlignLast::End => text3::cache::TextAlign::End,
StyleTextAlignLast::Left => text3::cache::TextAlign::Left,
StyleTextAlignLast::Right => text3::cache::TextAlign::Right,
StyleTextAlignLast::Center => text3::cache::TextAlign::Center,
StyleTextAlignLast::Justify => text3::cache::TextAlign::Justify,
},
word_break: match word_break_css {
StyleWordBreak::Normal | StyleWordBreak::BreakWord => text3::cache::WordBreak::Normal,
StyleWordBreak::BreakAll => text3::cache::WordBreak::BreakAll,
StyleWordBreak::KeepAll => text3::cache::WordBreak::KeepAll,
},
line_break: match line_break_css {
StyleLineBreak::Auto => text3::cache::LineBreakStrictness::Auto,
StyleLineBreak::Loose => text3::cache::LineBreakStrictness::Loose,
StyleLineBreak::Normal => text3::cache::LineBreakStrictness::Normal,
StyleLineBreak::Strict => text3::cache::LineBreakStrictness::Strict,
StyleLineBreak::Anywhere => text3::cache::LineBreakStrictness::Anywhere,
},
}
}
#[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>,
row_baselines: Vec<f32>,
border_collapse: StyleBorderCollapse,
border_spacing: LayoutBorderSpacing,
caption_index: Option<usize>,
collapsed_rows: std::collections::HashSet<usize>,
collapsed_columns: std::collections::HashSet<usize>,
hidden_empty_rows: std::collections::HashSet<usize>,
row_node_indices: Vec<usize>,
}
impl TableLayoutContext {
fn new() -> Self {
Self {
columns: Vec::new(),
cells: Vec::new(),
num_rows: 0,
use_fixed_layout: false,
row_heights: Vec::new(),
row_baselines: 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(),
hidden_empty_rows: std::collections::HashSet::new(),
row_node_indices: Vec::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: &LayoutNodeHot,
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 = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state.clone();
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(ctx.viewport_size.width, ctx.viewport_size.height),
};
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: &LayoutNodeHot,
) -> 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 = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state.clone();
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: &LayoutNodeHot,
) -> 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 = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state.clone();
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: &LayoutNodeHot,
) -> 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 = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state.clone();
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_empty_cells_property<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
node: &LayoutNodeHot,
) -> StyleEmptyCells {
let Some(dom_id) = node.dom_node_id else {
return StyleEmptyCells::Show;
};
let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state.clone();
ctx.styled_dom
.css_property_cache
.ptr
.get_empty_cells(node_data, &dom_id, &node_state)
.and_then(|prop| prop.get_property().copied())
.unwrap_or(StyleEmptyCells::Show)
}
fn get_caption_side_property<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
node: &LayoutNodeHot,
) -> 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 = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state.clone();
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: &LayoutNodeHot,
) -> bool {
if let Some(dom_id) = node.dom_node_id {
let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state.clone();
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 {
if tree.get(cell_index).is_none() {
return true; }
if tree.children(cell_index).is_empty() {
return true;
}
if let Some(warm_node) = tree.warm(cell_index) {
if let Some(ref cached_layout) = warm_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 = tree.warm(node_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
let containing_block_size = LogicalSize {
width: constraints.available_size.width,
height: constraints.available_size.height,
};
let table_bp = table_node.box_props.unpack();
let table_size = crate::solver3::sizing::calculate_used_size_for_node(
ctx.styled_dom,
Some(dom_id),
containing_block_size,
intrinsic,
&table_bp,
ctx.viewport_size,
)?;
table_size.width
} else {
constraints.available_size.width
};
let tbp = table_node.box_props.unpack();
let table_content_box_width = {
let padding_width = tbp.padding.left + tbp.padding.right;
let border_width = tbp.border.left + tbp.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}",
tbp.padding.left,
tbp.padding.right
);
debug_table_layout!(
ctx,
"Table border: L={:.2} R={:.2}",
tbp.border.left,
tbp.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, tree, &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(ctx.viewport_size.width, ctx.viewport_size.height),
};
let h_spacing = table_ctx
.border_spacing
.horizontal
.resolve_with_context(&spacing_context, PropertyContext::Other)
.max(0.0);
let v_spacing = table_ctx
.border_spacing
.vertical
.resolve_with_context(&spacing_context, PropertyContext::Other)
.max(0.0);
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 {
let full_spacings = (table_ctx.num_rows + 1) as f32;
let hidden_empty_count = table_ctx.hidden_empty_rows.len() as f32;
table_height += v_spacing * (full_spacings - hidden_empty_count);
}
}
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,
writing_mode_ctx: constraints.writing_mode_ctx,
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 = HashMap::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 tree.children(table_index) {
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 tree.children(child_idx) {
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 tree.children(colgroup_index) {
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 table_ctx.row_node_indices.len() <= row_num {
table_ctx.row_node_indices.resize(row_num + 1, 0);
}
table_ctx.row_node_indices[row_num] = row_index;
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 tree.children(row_index) {
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>,
tree: &LayoutTree,
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 mut col_has_width = vec![false; num_cols];
for cell_info in &table_ctx.cells {
if cell_info.row != 0 {
continue; }
if table_ctx.collapsed_columns.contains(&cell_info.column) {
continue;
}
let dom_id = match tree.get(cell_info.node_index).and_then(|n| n.dom_node_id) {
Some(id) => id,
None => continue,
};
let node_state = &ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
let css_width = get_css_width(ctx.styled_dom, dom_id, &node_state);
let explicit_px = match css_width.unwrap_or_default() {
LayoutWidth::Px(px) => {
resolve_size_metric(
px.metric,
px.number.get(),
available_width,
ctx.viewport_size,
)
}
LayoutWidth::Auto | LayoutWidth::MinContent | LayoutWidth::MaxContent
| LayoutWidth::Calc(_) | LayoutWidth::FitContent(_) => continue,
};
if cell_info.colspan == 1 {
table_ctx.columns[cell_info.column].computed_width = Some(explicit_px);
col_has_width[cell_info.column] = true;
} else {
let mut visible_span_count = 0;
for offset in 0..cell_info.colspan {
let col_idx = cell_info.column + offset;
if col_idx < num_cols && !table_ctx.collapsed_columns.contains(&col_idx) {
visible_span_count += 1;
}
}
if visible_span_count > 0 {
let per_col = explicit_px / visible_span_count as f32;
for offset in 0..cell_info.colspan {
let col_idx = cell_info.column + offset;
if col_idx < num_cols
&& !table_ctx.collapsed_columns.contains(&col_idx)
&& !col_has_width[col_idx]
{
table_ctx.columns[col_idx].computed_width = Some(per_col);
col_has_width[col_idx] = true;
}
}
}
}
}
let used_width: f32 = table_ctx.columns.iter().enumerate()
.filter(|(idx, _)| col_has_width[*idx] && !table_ctx.collapsed_columns.contains(idx))
.filter_map(|(_, c)| c.computed_width)
.sum();
let remaining_width = (available_width - used_width).max(0.0);
let num_remaining = table_ctx.columns.iter().enumerate()
.filter(|(idx, _)| !col_has_width[*idx] && !table_ctx.collapsed_columns.contains(idx))
.count();
if num_remaining > 0 {
let width_per_remaining = remaining_width / num_remaining 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 if !col_has_width[col_idx] {
col.computed_width = Some(width_per_remaining);
}
}
}
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);
}
}
let total_col_width: f32 = table_ctx.columns.iter()
.filter_map(|c| c.computed_width)
.sum();
if available_width > total_col_width && num_visible_cols > 0 {
let extra = available_width - total_col_width;
let extra_per_col = extra / num_visible_cols as f32;
for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
if !table_ctx.collapsed_columns.contains(&col_idx) {
if let Some(ref mut w) = col.computed_width {
*w += extra_per_col;
}
}
}
}
}
fn clear_subtree_cache(
tree: &LayoutTree,
cache_map: &mut crate::solver3::cache::LayoutCacheMap,
root: usize,
) {
if root < cache_map.entries.len() {
cache_map.entries[root].clear();
}
let child_ids: Vec<usize> = tree.children(root).to_vec();
for child in child_ids {
clear_subtree_cache(tree, cache_map, child);
}
}
fn measure_cell_content_width<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut crate::font_traits::TextLayoutCache,
cell_index: usize,
constraints: &LayoutConstraints,
sizing_mode: crate::text3::cache::AvailableSpace,
) -> Result<f32> {
let width_type = match sizing_mode {
crate::text3::cache::AvailableSpace::MinContent => Text3AvailableSpace::MinContent,
crate::text3::cache::AvailableSpace::MaxContent => Text3AvailableSpace::MaxContent,
crate::text3::cache::AvailableSpace::Definite(w) => Text3AvailableSpace::Definite(w),
};
let cell_constraints = LayoutConstraints {
available_size: LogicalSize {
width: sizing_mode.to_f32_for_layout(),
height: f32::INFINITY,
},
writing_mode: constraints.writing_mode,
writing_mode_ctx: constraints.writing_mode_ctx,
bfc_state: None,
text_align: constraints.text_align,
containing_block_size: constraints.containing_block_size,
available_width_type: width_type,
};
let mut temp_positions: super::PositionVec = Vec::new();
let mut temp_scrollbar_reflow = false;
let mut temp_float_cache = HashMap::new();
clear_subtree_cache(tree, &mut ctx.cache_map, cell_index);
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::ComputeSize,
)?;
let cell_bp = tree.get(cell_index)
.ok_or(LayoutError::InvalidTree)?
.box_props.unpack();
let padding = &cell_bp.padding;
let border = &cell_bp.border;
let wm = constraints.writing_mode;
let content_width = tree.warm(cell_index)
.and_then(|w| w.overflow_content_size)
.map(|s| s.width)
.unwrap_or_else(|| {
tree.get(cell_index)
.and_then(|n| n.used_size)
.map(|s| s.width)
.unwrap_or(0.0)
});
Ok(content_width
+ padding.cross_start(wm) + padding.cross_end(wm)
+ border.cross_start(wm) + border.cross_end(wm))
}
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> {
measure_cell_content_width(
ctx, tree, text_cache, cell_index, constraints,
crate::text3::cache::AvailableSpace::MinContent,
)
}
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> {
measure_cell_content_width(
ctx, tree, text_cache, cell_index, constraints,
crate::text3::cache::AvailableSpace::MaxContent,
)
}
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 = if total_min_width > 0.0 { available_width / total_min_width } else { 1.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 * 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 cell_bp = cell_node.box_props.unpack();
let padding = &cell_bp.padding;
let border = &cell_bp.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,
writing_mode_ctx: constraints.writing_mode_ctx,
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)?;
let cell_children: Vec<usize> = tree.children(cell_index).to_vec();
for child_idx in cell_children {
if let Some(warm) = tree.warm_mut(child_idx) {
warm.inline_layout_result = None;
}
}
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,
writing_mode_ctx: constraints.writing_mode_ctx,
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 = HashMap::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 cell_bp = cell_node.box_props.unpack();
let padding = &cell_bp.padding;
let border = &cell_bp.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 compute_cell_baseline(cell_index: usize, tree: &LayoutTree) -> f32 {
let Some(cell_node) = tree.get(cell_index) else {
return 0.0;
};
let cell_bp = cell_node.box_props.unpack();
if let Some(warm_node) = tree.warm(cell_index) {
if let Some(ref cached_layout) = warm_node.inline_layout_result {
let inline_result = &cached_layout.layout;
if let Some(first_item) = inline_result.items.first() {
let (item_ascent, _) = crate::text3::cache::get_item_vertical_metrics_approx(&first_item.item);
let padding_top = cell_bp.padding.top;
let border_top = cell_bp.border.top;
return padding_top + border_top + first_item.position.y + item_ascent;
}
}
}
let children = tree.children(cell_index);
for &child_idx in children {
if child_idx < tree.nodes.len() {
if let Some(child_warm) = tree.warm(child_idx) {
if child_warm.inline_layout_result.is_some() {
let child_baseline = compute_cell_baseline(child_idx, tree);
let padding_top = cell_bp.padding.top;
let border_top = cell_bp.border.top;
return padding_top + border_top + child_baseline;
}
}
}
}
let used_size = cell_node.used_size.unwrap_or_default();
let padding_bottom = cell_bp.padding.bottom;
let border_bottom = cell_bp.border.bottom;
used_size.height - padding_bottom - border_bottom
}
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];
table_ctx.row_baselines = 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);
}
if cell_info.rowspan == 1 {
let cell_baseline = compute_cell_baseline(cell_info.node_index, tree);
let current_baseline = table_ctx.row_baselines[cell_info.row];
table_ctx.row_baselines[cell_info.row] = current_baseline.max(cell_baseline);
}
}
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;
}
}
if table_ctx.border_collapse == StyleBorderCollapse::Separate {
for row_idx in 0..table_ctx.num_rows {
if table_ctx.collapsed_rows.contains(&row_idx) {
continue;
}
let row_cells: Vec<usize> = table_ctx
.cells
.iter()
.filter(|c| c.row == row_idx && c.rowspan == 1)
.map(|c| c.node_index)
.collect();
if row_cells.is_empty() {
continue;
}
let all_hidden_empty = row_cells.iter().all(|&cell_idx| {
if let Some(cell_node) = tree.get(cell_idx) {
let ec = get_empty_cells_property(ctx, cell_node);
ec == StyleEmptyCells::Hide && is_cell_empty(tree, cell_idx)
} else {
true
}
});
if all_hidden_empty {
table_ctx.row_heights[row_idx] = 0.0;
table_ctx.hidden_empty_rows.insert(row_idx);
}
}
}
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(ctx.viewport_size.width, ctx.viewport_size.height),
};
let h = table_ctx
.border_spacing
.horizontal
.resolve_with_context(&spacing_context, PropertyContext::Other)
.max(0.0);
let v = table_ctx
.border_spacing
.vertical
.resolve_with_context(&spacing_context, PropertyContext::Other)
.max(0.0);
(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 {
if table_ctx.collapsed_columns.contains(&i) {
} else {
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;
if table_ctx.collapsed_rows.contains(&i) {
} else if table_ctx.hidden_empty_rows.contains(&i) {
y_offset += height; } else {
y_offset += height + v_spacing; }
}
{
let total_col_width: f32 = table_ctx.columns.iter().map(|c| c.computed_width.unwrap_or(0.0)).sum::<f32>()
+ h_spacing * (table_ctx.columns.len().max(1) - 1) as f32
+ h_spacing * 2.0; for (i, &row_y) in row_positions.iter().enumerate() {
if let Some(&row_node_idx) = table_ctx.row_node_indices.get(i) {
let row_height = table_ctx.row_heights.get(i).copied().unwrap_or(0.0);
if let Some(row_node) = tree.get_mut(row_node_idx) {
row_node.used_size = Some(LogicalSize {
width: total_col_width,
height: row_height,
});
}
}
}
}
for cell_info in &table_ctx.cells {
let precomputed_cell_baseline = compute_cell_baseline(cell_info.node_index, tree);
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
);
let cell_dom_node_id = cell_node.dom_node_id;
let cell_box_props = cell_node.box_props.unpack();
drop(cell_node);
let vertical_align_adjustment = if let Some(warm_node) = tree.warm(cell_info.node_index) {
if let Some(ref cached_layout) = warm_node.inline_layout_result {
let inline_result = &cached_layout.layout;
use StyleVerticalAlign;
let vertical_align = if let Some(dom_id) = cell_dom_node_id {
let node_state = ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state.clone();
match get_vertical_align_property(ctx.styled_dom, dom_id, &node_state) {
MultiValue::Exact(v) => v,
_ => StyleVerticalAlign::Baseline,
}
} else {
StyleVerticalAlign::Baseline
};
let content_bounds = inline_result.bounds();
let content_height = content_bounds.height;
let padding = &cell_box_props.padding;
let border = &cell_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 y_offset = match vertical_align {
StyleVerticalAlign::Top => 0.0,
StyleVerticalAlign::Middle => (content_box_height - content_height) * 0.5,
StyleVerticalAlign::Bottom => content_box_height - content_height,
StyleVerticalAlign::Baseline
| StyleVerticalAlign::Sub
| StyleVerticalAlign::Superscript
| StyleVerticalAlign::TextTop
| StyleVerticalAlign::TextBottom
| StyleVerticalAlign::Percentage(_)
| StyleVerticalAlign::Length(_) => {
let row_baseline = table_ctx.row_baselines.get(cell_info.row).copied().unwrap_or(0.0);
(row_baseline - precomputed_cell_baseline).max(0.0)
}
};
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 {
Some((y_offset, cached_layout.available_width, cached_layout.has_floats))
} else {
None
}
} else {
None
}
} else {
None
};
if let Some((y_offset, available_width, has_floats)) = vertical_align_adjustment {
if let Some(warm_mut) = tree.warm_mut(cell_info.node_index) {
if let Some(ref cached_layout) = warm_mut.inline_layout_result {
use std::sync::Arc;
use crate::text3::cache::{PositionedItem, UnifiedLayout};
let adjusted_items: Vec<PositionedItem> = cached_layout.layout
.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: cached_layout.layout.overflow.clone(),
};
warm_mut.inline_layout_result = Some(CachedInlineLayout::new(
Arc::new(adjusted_layout),
available_width,
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(cold_node) = tree.cold_mut(ifc_root_index) {
cold_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 tree.children(ifc_root_index)
.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<_> = tree.children(ifc_root_index).to_vec();
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(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height)));
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(warm_mut) = tree.warm_mut(child_index) {
warm_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 = tree.warm(child_index).and_then(|w| w.intrinsic_sizes.clone()).unwrap_or_default();
let box_props = child_node.box_props.unpack();
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_wm_ctx = super::geometry::WritingModeContext::new(
writing_mode,
get_direction_property(ctx.styled_dom, dom_id, &styled_node_state)
.unwrap_or_default(),
get_text_orientation_property(ctx.styled_dom, dom_id, &styled_node_state)
.unwrap_or_default(),
);
let child_constraints = LayoutConstraints {
available_size: LogicalSize::new(content_box_size.width, f32::INFINITY),
writing_mode,
writing_mode_ctx: child_wm_ctx,
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 = HashMap::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 overflow_x = get_overflow_x(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
let overflow_is_visible = matches!(
(overflow_x, overflow_y),
(LayoutOverflow::Visible, LayoutOverflow::Visible)
);
let baseline_offset = if overflow_is_visible {
layout_result.output.baseline.unwrap_or(final_height)
} else {
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(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height));
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(|(idx, node)| {
node.dom_node_id == Some(list_dom_id) && tree.warm(*idx).and_then(|w| w.pseudo_element).is_none()
})
.map(|(idx, _)| idx);
if let Some(list_idx) = list_item_layout_idx {
let marker_idx = tree.children(list_idx)
.iter()
.find(|&&child_idx| {
tree.warm(child_idx)
.map(|w| w.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(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height)));
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(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height)));
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(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height)));
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(warm_mut) = tree.warm_mut(layout_idx) {
warm_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 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 = tree.warm(child_index).and_then(|w| w.intrinsic_sizes.clone()).unwrap_or_default();
let box_props = child_node.box_props.unpack();
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_wm_ctx = super::geometry::WritingModeContext::new(
writing_mode,
get_direction_property(ctx.styled_dom, dom_id, &styled_node_state)
.unwrap_or_default(),
get_text_orientation_property(ctx.styled_dom, dom_id, &styled_node_state)
.unwrap_or_default(),
);
let child_constraints = LayoutConstraints {
available_size: LogicalSize::new(content_box_size.width, f32::INFINITY),
writing_mode,
writing_mode_ctx: child_wm_ctx,
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 = HashMap::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 overflow_x = get_overflow_x(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
let overflow_is_visible = matches!(
(overflow_x, overflow_y),
(LayoutOverflow::Visible, LayoutOverflow::Visible)
);
let baseline_from_top = layout_result.output.baseline;
let baseline_offset = match baseline_from_top {
Some(baseline_y) if overflow_is_visible => {
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)
}
_ => {
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.unpack();
let intrinsic_size = tree.warm(child_index)
.and_then(|w| w.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.as_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(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height));
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()
);
if span_dom_children.is_empty() {
let node_state = &ctx.styled_dom.styled_nodes.as_container()[span_dom_id].styled_node_state;
let font_size = get_element_font_size(ctx.styled_dom, span_dom_id, node_state);
let line_height_value = crate::solver3::getters::get_line_height_value(
ctx.styled_dom, span_dom_id, &node_state
);
let line_height = line_height_value
.map(|v| text3::cache::LineHeight::Px(v.inner.normalized() * font_size))
.unwrap_or(text3::cache::LineHeight::Normal);
let cb_width = constraints.containing_block_size.main(constraints.writing_mode);
let padding_top = crate::solver3::getters::get_css_padding_top(ctx.styled_dom, span_dom_id, &node_state)
.exact().map(|pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE)).unwrap_or(0.0);
let padding_bottom = crate::solver3::getters::get_css_padding_bottom(ctx.styled_dom, span_dom_id, &node_state)
.exact().map(|pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE)).unwrap_or(0.0);
let padding_left = crate::solver3::getters::get_css_padding_left(ctx.styled_dom, span_dom_id, &node_state)
.exact().map(|pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE)).unwrap_or(0.0);
let padding_right = crate::solver3::getters::get_css_padding_right(ctx.styled_dom, span_dom_id, &node_state)
.exact().map(|pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE)).unwrap_or(0.0);
let border_top = crate::solver3::getters::get_css_border_top_width(ctx.styled_dom, span_dom_id, &node_state)
.exact().map(|pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE)).unwrap_or(0.0);
let border_bottom = crate::solver3::getters::get_css_border_bottom_width(ctx.styled_dom, span_dom_id, &node_state)
.exact().map(|pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE)).unwrap_or(0.0);
let border_left = crate::solver3::getters::get_css_border_left_width(ctx.styled_dom, span_dom_id, &node_state)
.exact().map(|pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE)).unwrap_or(0.0);
let border_right = crate::solver3::getters::get_css_border_right_width(ctx.styled_dom, span_dom_id, &node_state)
.exact().map(|pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE)).unwrap_or(0.0);
let margin_left = crate::solver3::getters::get_css_margin_left(ctx.styled_dom, span_dom_id, &node_state)
.exact().map(|pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE)).unwrap_or(0.0);
let margin_right = crate::solver3::getters::get_css_margin_right(ctx.styled_dom, span_dom_id, &node_state)
.exact().map(|pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE)).unwrap_or(0.0);
let resolved_line_height = line_height.resolve(font_size, 0.0, 0.0, 0.0, 0);
let total_height = resolved_line_height + padding_top + padding_bottom + border_top + border_bottom;
let total_width = margin_left + padding_left + border_left
+ border_right + padding_right + margin_right;
content.push(InlineContent::Shape(InlineShape {
shape_def: ShapeDefinition::Rectangle {
size: crate::text3::cache::Size {
width: total_width,
height: total_height,
},
corner_radius: None,
},
fill: None,
stroke: None,
baseline_offset: 0.0,
alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, span_dom_id),
source_node_id: Some(span_dom_id),
}));
return Ok(());
}
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(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height));
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 = tree.warm(child_index).and_then(|w| w.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_wm_ctx = super::geometry::WritingModeContext::new(
writing_mode,
get_direction_property(ctx.styled_dom, child_dom_id, &styled_node_state)
.unwrap_or_default(),
get_text_orientation_property(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,
writing_mode_ctx: child_wm_ctx,
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 = HashMap::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 overflow_x = get_overflow_x(ctx.styled_dom, child_dom_id, &styled_node_state).unwrap_or_default();
let overflow_y = get_overflow_y(ctx.styled_dom, child_dom_id, &styled_node_state).unwrap_or_default();
let overflow_is_visible = matches!(
(overflow_x, overflow_y),
(LayoutOverflow::Visible, LayoutOverflow::Visible)
);
let baseline_offset = if overflow_is_visible {
layout_result.output.baseline.unwrap_or(final_height)
} else {
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] Inlinifying block-level child {:?} \
(display: {:?}) inside inline span per css-display-3 §2.7",
child_dom_id,
child_display
);
let child_style = get_style_properties(ctx.styled_dom, child_dom_id, ctx.system_style.as_ref(), PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height));
collect_inline_span_recursive(
ctx,
tree,
child_dom_id,
child_style,
content,
child_map,
parent_children,
constraints,
)?;
}
}
}
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 => {
return Err(LayoutError::PositioningFailed);
}
};
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
},
visual_width_px: 0.0,
}
}
pub 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(tree: &LayoutTree, node_index: usize) -> bool {
let node = match tree.get(node_index) {
Some(n) => n,
None => return true,
};
if !tree.children(node_index).is_empty() {
return false;
}
if tree.warm(node_index).and_then(|w| w.inline_layout_result.as_ref()).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: &HashMap<(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(),
};
let marker_pseudo = tree.warm(marker_index).and_then(|w| w.pseudo_element);
let marker_anonymous_type = tree.cold(marker_index).and_then(|c| c.anonymous_type);
if marker_pseudo != 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_pseudo, marker_anonymous_type
)));
}
if marker_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: &HashMap<(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,
}]
}
#[inline]
fn is_bk_or_nl_class(c: char) -> bool {
matches!(c, '\u{000B}' | '\u{000C}' | '\u{0085}' | '\u{2028}' | '\u{2029}')
}
fn split_at_forced_breaks(text: &str) -> Vec<String> {
let mut segments = Vec::new();
let mut current = String::new();
let mut chars = text.chars().peekable();
while let Some(c) = chars.next() {
if c == '\n' {
segments.push(std::mem::take(&mut current));
} else if c == '\r' {
segments.push(std::mem::take(&mut current));
if chars.peek() == Some(&'\n') {
chars.next();
}
} else if is_bk_or_nl_class(c) {
segments.push(std::mem::take(&mut current));
} else {
current.push(c);
}
}
segments.push(current);
segments
}
fn split_at_bk_nl_chars(text: &str) -> Vec<String> {
let mut segments = Vec::new();
let mut current = String::new();
for c in text.chars() {
if is_bk_or_nl_class(c) {
segments.push(std::mem::take(&mut current));
} else {
current.push(c);
}
}
segments.push(current);
segments
}
fn is_east_asian_wide(c: char) -> bool {
let cp = c as u32;
(0x4E00..=0x9FFF).contains(&cp)
|| (0x3400..=0x4DBF).contains(&cp)
|| (0x20000..=0x2A6DF).contains(&cp)
|| (0xF900..=0xFAFF).contains(&cp)
|| (0x3040..=0x309F).contains(&cp)
|| (0x30A0..=0x30FF).contains(&cp)
|| (0x31F0..=0x31FF).contains(&cp)
|| (0x2E80..=0x2EFF).contains(&cp)
|| (0x2F00..=0x2FDF).contains(&cp)
|| (0x2FF0..=0x2FFF).contains(&cp)
|| (0x3000..=0x303F).contains(&cp)
|| (0x3200..=0x32FF).contains(&cp)
|| (0x3300..=0x33FF).contains(&cp)
|| (0x3100..=0x312F).contains(&cp)
|| (0xAC00..=0xD7AF).contains(&cp)
|| (0xFF01..=0xFF60).contains(&cp)
|| (0xFFE0..=0xFFE6).contains(&cp)
}
fn is_east_asian_fullwidth_or_wide(ch: char) -> bool {
let cp = ch as u32;
if (0x1100..=0x11FF).contains(&cp)
|| (0x3130..=0x318F).contains(&cp)
|| (0xAC00..=0xD7AF).contains(&cp)
|| (0xA960..=0xA97F).contains(&cp)
|| (0xD7B0..=0xD7FF).contains(&cp)
{
return false;
}
is_east_asian_wide(ch)
|| (0xFF61..=0xFFDC).contains(&cp)
|| (0xFFE8..=0xFFEE).contains(&cp)
|| (0xA000..=0xA4CF).contains(&cp)
}
fn apply_segment_break_transform(text: &str) -> String {
let chars: Vec<char> = text.chars().collect();
let len = chars.len();
let mut result = String::with_capacity(text.len());
let mut i = 0;
while i < len {
let ch = chars[i];
if ch == '\n' || ch == '\r' {
let break_end = if ch == '\r' && i + 1 < len && chars[i + 1] == '\n' {
i + 2
} else {
i + 1
};
while result.ends_with(' ') || result.ends_with('\t') {
result.pop();
}
let mut after_idx = break_end;
while after_idx < len && (chars[after_idx] == ' ' || chars[after_idx] == '\t') {
after_idx += 1;
}
let char_before = result.chars().last();
let char_after = if after_idx < len { Some(chars[after_idx]) } else { None };
if char_before == Some('\u{200B}') || char_after == Some('\u{200B}') {
}
else if let (Some(before), Some(after)) = (char_before, char_after) {
if is_east_asian_fullwidth_or_wide(before) && is_east_asian_fullwidth_or_wide(after) {
} else {
result.push(' ');
}
} else {
result.push(' ');
}
i = after_idx;
} else {
result.push(ch);
i += 1;
}
}
result
}
pub fn ws_phase1_collapse(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut prev_was_space = false;
for ch in text.chars() {
if ch == ' ' || ch == '\t' {
if !prev_was_space {
result.push(' ');
prev_was_space = true;
}
} else {
result.push(ch);
prev_was_space = false;
}
}
result
}
pub fn ws_phase2_segment_break_transform(text: &str) -> String {
apply_segment_break_transform(text)
}
pub fn ws_phase3_trim_edges(text: &str) -> String {
text.to_string()
}
pub fn ws_phase4_resolve_tabs(text: &str) -> String {
text.to_string()
}
fn is_bidi_control(c: char) -> bool {
matches!(c,
'\u{200E}' | '\u{200F}' | '\u{202A}' | '\u{202B}' | '\u{202C}' | '\u{202D}' | '\u{202E}' | '\u{2066}' | '\u{2067}' | '\u{2068}' | '\u{2069}' | '\u{061C}' )
}
#[inline]
pub fn is_css_document_whitespace(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')
}
pub 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 text_owned;
let text: &str = if text.chars().any(|c| is_bidi_control(c)) {
text_owned = text.chars().filter(|c| !is_bidi_control(*c)).collect::<String>();
&text_owned
} else {
text
};
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();
let text_cr;
let text: &str = if text.contains('\r') {
text_cr = text.replace("\r\n", "\n").replace('\r', "\n");
&text_cr
} else {
text
};
match white_space {
StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap | StyleWhiteSpace::BreakSpaces => {
let segments = split_at_forced_breaks(text);
let segment_count = segments.len();
let mut content_index = 0;
for (seg_idx, segment) in segments.into_iter().enumerate() {
let mut tab_parts = segment.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 seg_idx + 1 < segment_count {
result.push(InlineContent::LineBreak(InlineBreak {
break_type: BreakType::Hard,
clear: ClearType::None,
content_index,
}));
content_index += 1;
}
}
}
StyleWhiteSpace::PreLine => {
let segments = split_at_forced_breaks(text);
let segment_count = segments.len();
let mut content_index = 0;
for (seg_idx, segment) in segments.into_iter().enumerate() {
let collapsed: String = segment
.split(|c: char| is_css_document_whitespace(c))
.filter(|s| !s.is_empty())
.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 seg_idx + 1 < segment_count {
result.push(InlineContent::LineBreak(InlineBreak {
break_type: BreakType::Hard,
clear: ClearType::None,
content_index,
}));
content_index += 1;
}
}
}
StyleWhiteSpace::Normal | StyleWhiteSpace::Nowrap => {
let segments = split_at_bk_nl_chars(text);
let segment_count = segments.len();
let mut content_index = 0;
for (seg_idx, segment) in segments.into_iter().enumerate() {
let after_segment_breaks = apply_segment_break_transform(&segment);
let collapsed: String = after_segment_breaks
.chars()
.map(|c| if is_css_document_whitespace(c) { ' ' } else { c })
.collect::<String>()
.split(' ')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(" ");
let final_text = if collapsed.is_empty() && !segment.is_empty() {
" ".to_string()
} else if !collapsed.is_empty() {
let had_leading = segment.chars().next().map(|c| is_css_document_whitespace(c)).unwrap_or(false);
let had_trailing = segment.chars().last().map(|c| is_css_document_whitespace(c)).unwrap_or(false);
let mut r = String::new();
if had_leading { r.push(' '); }
r.push_str(&collapsed);
if had_trailing && !had_leading { r.push(' '); }
else if had_trailing && had_leading && collapsed.is_empty() { }
else if had_trailing { r.push(' '); }
r
} else {
collapsed
};
if !final_text.is_empty() {
result.push(InlineContent::Text(StyledRun {
text: final_text,
style: Arc::clone(&style),
logical_start_byte: 0,
source_node_id: Some(dom_id),
}));
}
if seg_idx + 1 < segment_count {
result.push(InlineContent::LineBreak(InlineBreak {
break_type: BreakType::Hard,
clear: ClearType::None,
content_index,
}));
content_index += 1;
}
}
}
}
let text_transform = style.text_transform;
if text_transform != crate::text3::cache::TextTransform::None {
for item in result.iter_mut() {
if let InlineContent::Text(run) = item {
run.text = apply_text_transform(&run.text, text_transform);
}
}
}
result
}
fn apply_text_transform(text: &str, transform: crate::text3::cache::TextTransform) -> String {
use crate::text3::cache::TextTransform;
match transform {
TextTransform::None => text.to_string(),
TextTransform::Uppercase => text.to_uppercase(),
TextTransform::Lowercase => text.to_lowercase(),
TextTransform::Capitalize => {
let mut result = String::with_capacity(text.len());
let mut prev_is_word_boundary = true;
for c in text.chars() {
if prev_is_word_boundary && c.is_alphabetic() {
for uc in c.to_uppercase() {
result.push(uc);
}
prev_is_word_boundary = false;
} else {
result.push(c);
prev_is_word_boundary = c.is_whitespace() || c.is_ascii_punctuation();
}
}
result
}
TextTransform::FullWidth => {
text.chars().map(|c| match c {
' ' => '\u{3000}', '!' ..= '~' => {
char::from_u32(c as u32 - 0x0021 + 0xFF01).unwrap_or(c)
}
_ => c,
}).collect()
}
}
}
pub fn layout_initial_letter(
initial_letter_size: f32,
initial_letter_sink: u32,
content_box_width: f32,
line_height: f32,
) -> (f32, f32) {
if initial_letter_size <= 0.0 || line_height <= 0.0 || content_box_width <= 0.0 {
return (0.0, 0.0);
}
let letter_height = initial_letter_size * line_height;
const CAP_WIDTH_RATIO: f32 = 0.7;
let letter_width_raw = letter_height * CAP_WIDTH_RATIO;
const LETTER_GAP: f32 = 4.0;
let letter_width = (letter_width_raw + LETTER_GAP).min(content_box_width);
let exclusion_height = (initial_letter_sink as f32) * line_height;
let effective_height = exclusion_height.max(letter_height);
(letter_width, effective_height)
}