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_box_sizing,
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, BreakType, ClearType, InlineBreak,
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(crate) const 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 {
#[must_use] pub const fn is_clipped(&self) -> bool {
matches!(self, Self::Hidden | Self::Clip | Self::Scroll | Self::Auto)
}
#[must_use] pub const 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 Default for BfcState {
fn default() -> Self {
Self::new()
}
}
impl BfcState {
#[must_use] pub fn new() -> Self {
Self {
pen: LogicalPosition::zero(),
floats: FloatingContext::default(),
margins: MarginCollapseContext::default(),
}
}
}
#[derive(Copy, 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 });
}
#[must_use] 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)
}
#[must_use] 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,
}
#[allow(clippy::implicit_hasher)] pub fn layout_formatting_context<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut TextLayoutCache,
node_index: usize,
constraints: &LayoutConstraints<'_>,
float_cache: &mut HashMap<usize, FloatingContext>,
) -> Result<BfcLayoutResult> {
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark(((0x609E0 + (node_index & 7) * 4)) as u32, (0xC0DE0042) as u32); }
let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark(((0x60B80 + (node_index & 7) * 4)) as u32, ((node as *const _ as usize) as u32) as u32); }
#[cfg(feature = "web_lift")]
{
let force_ifc = node
.dom_node_id
.map_or(false, |dom_id| {
crate::solver3::layout_tree::has_only_inline_children(ctx.styled_dom, dom_id)
});
if force_ifc {
unsafe { crate::az_mark(((0x60BA0 + (node_index & 7) * 4)) as u32, (0xC0DE1FC0) as u32); }
return layout_ifc(ctx, text_cache, tree, node_index, constraints)
.map(BfcLayoutResult::from_output);
}
}
#[cfg(feature = "web_lift")]
unsafe {
let fc_disc = match node.formatting_context {
FormattingContext::Block { .. } => 1u32,
FormattingContext::Inline => 2,
FormattingContext::InlineBlock => 3,
FormattingContext::Flex => 4,
FormattingContext::Grid => 5,
FormattingContext::Table => 6,
FormattingContext::TableCell => 7,
FormattingContext::TableCaption => 8,
_ => 0,
};
crate::az_mark(((0x609A0 + (node_index & 7) * 4)) as u32, (fc_disc | 0xC0DE0000) as u32);
}
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 { .. } => {
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0001) as u32); }
layout_bfc(ctx, tree, text_cache, node_index, constraints, float_cache)
}
FormattingContext::Inline => {
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0002) as u32); }
layout_ifc(ctx, text_cache, tree, node_index, constraints)
.map(BfcLayoutResult::from_output)
}
FormattingContext::InlineBlock => {
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0003) as u32); }
let mut temp_float_cache = HashMap::new();
layout_bfc(ctx, tree, text_cache, node_index, constraints, &mut temp_float_cache)
}
FormattingContext::Table => {
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0006) as u32); }
layout_table_fc(ctx, tree, text_cache, node_index, constraints)
.map(BfcLayoutResult::from_output)
}
FormattingContext::Flex | FormattingContext::Grid => {
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0004) as u32); }
layout_flex_grid(ctx, tree, text_cache, node_index, constraints)
}
FormattingContext::TableCell | FormattingContext::TableCaption => {
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0007) as u32); }
let mut temp_float_cache = HashMap::new();
layout_bfc(ctx, tree, text_cache, node_index, constraints, &mut temp_float_cache)
}
_ => {
#[cfg(feature = "web_lift")]
unsafe {
crate::az_mark(((0x609C0 + (node_index & 7) * 4)) as u32, (0xC0DE0009) as u32);
let disc: u8 = core::ptr::read_volatile((&node.formatting_context) as *const FormattingContext as *const u8);
crate::az_mark(((0x60B40 + (node_index & 7) * 4)) as u32, (0xC0DE0000 | (disc as u32)) as u32);
}
let mut temp_float_cache = HashMap::new();
layout_bfc(
ctx,
tree,
text_cache,
node_index,
constraints,
&mut temp_float_cache,
)
}
}
}
#[allow(clippy::too_many_lines)] fn layout_flex_grid<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut 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 {
node.used_size.as_ref().map(|s| s.width)
};
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);
if !is_root {
let container_bb = translate_taffy_size_back(taffy_output.size);
if let Some(node_mut) = tree.get_mut(node_index) {
node_mut.used_size = Some(container_bb);
}
}
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))
}
#[derive(Clone, Copy)]
enum Axis {
Width,
Height,
}
fn border_box_to_content<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
node: &LayoutNodeHot,
id: NodeId,
node_state: &StyledNodeState,
resolved: f32,
is_percentage: bool,
axis: Axis,
) -> f32 {
if is_percentage {
return resolved;
}
let is_border_box = matches!(
get_css_box_sizing(ctx.styled_dom, id, node_state),
MultiValue::Exact(azul_css::props::layout::LayoutBoxSizing::BorderBox)
);
if !is_border_box {
return resolved;
}
let bp = node.box_props.unpack();
let adjustment = match axis {
Axis::Width => bp.border.left + bp.border.right + bp.padding.left + bp.padding.right,
Axis::Height => bp.border.top + bp.border.bottom + bp.padding.top + bp.padding.bottom,
};
(resolved - adjustment).max(0.0)
}
fn resolve_explicit_dimension_width<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
node: &LayoutNodeHot,
constraints: &LayoutConstraints<'_>,
) -> (Option<f32>, bool) {
node.dom_node_id
.map_or((None, false), |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
| LayoutWidth::MinContent
| LayoutWidth::MaxContent
| LayoutWidth::FitContent(_) => (None, false),
LayoutWidth::Px(px) => {
let node_state = &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state;
let pixels = resolve_size_metric(
px.metric,
px.number.get(),
constraints.available_size.width,
ctx.viewport_size,
get_element_font_size(ctx.styled_dom, id, node_state),
get_root_font_size(ctx.styled_dom, node_state),
);
let content_px = border_box_to_content(
ctx, node, id, node_state, pixels, px.metric == SizeMetric::Percent, Axis::Width,
);
(Some(content_px), true)
}
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);
let content_px = border_box_to_content(
ctx, node, id, node_state, px, false, Axis::Width,
);
(Some(content_px), true)
}
}
})
}
fn resolve_explicit_dimension_height<T: ParsedFontTrait>(
ctx: &LayoutContext<'_, T>,
node: &LayoutNodeHot,
constraints: &LayoutConstraints<'_>,
) -> (Option<f32>, bool) {
node.dom_node_id
.map_or((None, false), |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
| LayoutHeight::MinContent
| LayoutHeight::MaxContent
| LayoutHeight::FitContent(_) => (None, false),
LayoutHeight::Px(px) => {
let node_state = &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state;
let pixels = resolve_size_metric(
px.metric,
px.number.get(),
constraints.available_size.height,
ctx.viewport_size,
get_element_font_size(ctx.styled_dom, id, node_state),
get_root_font_size(ctx.styled_dom, node_state),
);
let content_px = border_box_to_content(
ctx, node, id, node_state, pixels, px.metric == SizeMetric::Percent, Axis::Height,
);
(Some(content_px), true)
}
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);
let content_px = border_box_to_content(
ctx, node, id, node_state, px, false, Axis::Height,
);
(Some(content_px), true)
}
}
})
}
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);
}
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);
}
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,
}
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn layout_bfc<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut 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 = node.used_size.map_or_else(
|| constraints.available_size,
|used_size| {
let inner = node.box_props.inner_size(used_size, writing_mode);
let height_is_auto = tree
.warm(node_index)
.is_none_or(|w| w.computed_style.height.is_none());
if height_is_auto {
LogicalSize::new(inner.width, constraints.available_size.height)
} else {
inner
}
},
);
let scrollbar_reservation = node
.dom_node_id
.map_or(0.0, |dom_id| {
let styled_node_state = ctx
.styled_dom
.styled_nodes
.as_container()
.get(dom_id)
.map(|s| s.styled_node_state)
.unwrap_or_default();
let overflow_y =
get_overflow_y(ctx.styled_dom, dom_id, &styled_node_state);
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())
.is_some_and(|s| s.needs_vertical);
if already_needs {
crate::solver3::getters::get_layout_scrollbar_width_px(ctx, dom_id, &styled_node_state)
} else {
0.0
}
}
_ => 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();
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark(((0x60A00 + (node_index & 7) * 4)) as u32, (bfc_children.len() as u32 | 0xC0DE0000) as u32); }
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;
}
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark(((0x60A40 + (child_index & 7) * 4)) as u32, (0xC0DE0000 | (child_index as u32 & 0xffff)) as u32); }
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;
}
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 = if let Some(size) = child_node.used_size { size } else {
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 {
main_pen + last_margin_bottom
} else {
float_context.clearance_offset(float_clear, main_pen + last_margin_bottom, writing_mode)
};
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;
}
}
if first_child_index.is_none() {
first_child_index = Some(child_index);
}
last_child_index = Some(child_index);
let child_size = if let Some(size) = child_node.used_size { size } else {
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) { None } else {
tree.warm(child_index).and_then(|w| w.escaped_top_margin)
};
let child_escaped_bottom = if has_margin_collapse_blocker(&child_bp, writing_mode, false) { None } else {
tree.warm(child_index).and_then(|w| w.escaped_bottom_margin)
};
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 {
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;
} else {
accumulated_top_margin = collapse_margins(parent_margin_top, self_collapsed);
}
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 {
false
} else {
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
}
};
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,
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)
.is_some_and(|cb_dom_id| {
let node_state = ctx.styled_dom.styled_nodes.as_container()
.get(cb_dom_id)
.map(|s| s.styled_node_state)
.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 if !parent_has_bottom_blocker && has_content {
debug_info!(
ctx,
"[layout_bfc] Bottom margin of blocked last child ESCAPES content box for node \
{}: last_margin_bottom={} (not added to height)",
node_index,
last_margin_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.values_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 = if is_root_node {
main_pen - total_escaped_top_margin - escaped_bottom_margin.unwrap_or(0.0)
} else {
main_pen
};
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,
})
}
#[allow(clippy::field_reassign_with_default)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn layout_ifc<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
text_cache: &mut TextLayoutCache,
tree: &mut LayoutTree,
node_index: usize,
constraints: &LayoutConstraints<'_>,
) -> Result<LayoutOutput> {
unsafe { crate::az_mark(0x60704_u32, (0x20u32)); }
#[cfg(feature = "web_lift")]
unsafe {
let slot = (node_index & 7) * 4;
crate::az_mark(((0x60900 + slot)) as u32, (tree.nodes.len() as u32) as u32);
crate::az_mark(((0x60920 + slot)) as u32, ((&*tree as *const LayoutTree as usize) as u32) as u32);
}
let float_count = constraints
.bfc_state
.as_ref()
.map_or(0, |s| s.floats.floats.len());
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 = if let Some(id) = node.dom_node_id { id } else {
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)).find_map(|n| n.dom_node_id)
.ok_or(LayoutError::InvalidTree)?
}
};
debug_ifc_layout!(ctx, "ifc_root_dom_id={:?}", ifc_root_dom_id);
let collect_result = collect_and_measure_inline_content(
ctx,
text_cache,
tree,
node_index,
constraints,
);
#[cfg(feature = "web_lift")]
unsafe {
crate::az_mark((0x60680) as u32, (collect_result.as_ref().map(|(c, _)| c.len()).unwrap_or(0) as u32) as u32);
crate::az_mark((0x60684) as u32, (if collect_result.is_ok() { 0xC0DE0680u32 } else { 0x000000EEu32 }) as u32);
}
let (inline_content, child_map) = collect_result?;
let text3_constraints =
translate_to_text3_constraints(ctx, constraints, ctx.styled_dom, ifc_root_dom_id);
let current_content_hash = {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
inline_content.hash(&mut h);
text3_constraints.text_align.hash(&mut h);
text3_constraints.text_align_last.hash(&mut h);
text3_constraints.white_space_mode.hash(&mut h);
text3_constraints.direction.hash(&mut h);
text3_constraints.columns.hash(&mut h);
text3_constraints.text_indent.to_bits().hash(&mut h);
match text3_constraints.line_height {
text3::cache::LineHeight::Normal => 0u64.hash(&mut h),
text3::cache::LineHeight::Px(v) => {
1u64.hash(&mut h);
v.to_bits().hash(&mut h);
}
}
h.finish()
};
debug_info!(
ctx,
"[layout_ifc] Collected {} inline content items for node {}",
inline_content.len(),
node_index
);
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!");
if let Some(warm_node) = tree.warm_mut(node_index) {
warm_node.inline_layout_result = None;
}
return Ok(LayoutOutput::default());
}
{
let cached_ifc = tree
.warm(node_index)
.and_then(|n| n.inline_layout_result.as_ref());
let resize_has_floats = constraints
.bfc_state
.as_ref()
.is_some_and(|s| !s.floats.floats.is_empty());
let cached_ifc = cached_ifc
.filter(|c| c.is_valid_for(constraints.available_width_type, resize_has_floats))
.filter(|c| c.inline_content_hash == current_content_hash);
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 = text3::cache::try_incremental_relayout(
&[], &old_advances,
&old_advances, line_breaks,
);
if matches!(result, 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 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 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) => {
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark((0x60688) as u32, (0xC0DE0688u32) as u32); }
result
}
Err(e) => {
#[cfg(feature = "web_lift")]
unsafe {
crate::az_mark((0x60688) as u32, (0x000000EEu32) as u32);
crate::az_mark((0x6068C) as u32, (*(&e as *const _ as *const u8)) as u32);
}
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 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()
.is_some_and(|s| !s.floats.floats.is_empty());
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)
|| cached.inline_content_hash != current_content_hash
{
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 {
let mut cil = CachedInlineLayout::new_with_constraints(
main_frag.clone(),
current_width_type,
has_floats,
cached_constraints.clone(),
);
cil.inline_content_hash = current_content_hash;
warm_node.inline_layout_result = Some(cil);
}
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()
.is_some_and(|cc| cc.dom_declared_flags & azul_css::compact_cache::DOM_HAS_TEXT_BOX_TRIM == 0);
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);
}
}
}
}
#[cfg(feature = "web_lift")]
unsafe {
crate::az_mark((0x60670) as u32, (output.overflow_size.width.to_bits()) as u32);
crate::az_mark((0x60674) as u32, (output.overflow_size.height.to_bits()) as u32);
crate::az_mark((0x60678) as u32, (output.positions.len() as u32) as u32);
crate::az_mark((0x6067C) as u32, (0xC0DE0132u32) as u32);
}
Ok(output)
}
const fn translate_taffy_size(size: LogicalSize) -> TaffySize<Option<f32>> {
TaffySize {
width: Some(size.width),
height: Some(size.height),
}
}
#[must_use] pub const 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,
}
}
#[must_use] pub const 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,
element_font_size: f32,
root_font_size: f32,
) -> f32 {
match metric {
SizeMetric::Px => value,
SizeMetric::Pt => value * PT_TO_PX,
SizeMetric::Percent => value / 100.0 * containing_block_size,
SizeMetric::Em => value * element_font_size,
SizeMetric::Rem => value * root_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 * super::calc::PX_PER_INCH,
SizeMetric::Cm => value * super::calc::PX_PER_INCH / super::calc::CM_PER_INCH,
SizeMetric::Mm => value * super::calc::PX_PER_INCH / super::calc::MM_PER_INCH,
}
}
#[must_use] pub const fn translate_taffy_size_back(size: TaffySize<f32>) -> LogicalSize {
LogicalSize {
width: size.width,
height: size.height,
}
}
#[must_use] pub const 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 = 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_display = get_display_property(ctx.styled_dom, Some(parent_dom_id));
if matches!(
parent_display,
MultiValue::Exact(
LayoutDisplay::Flex
| LayoutDisplay::InlineFlex
| LayoutDisplay::Grid
| LayoutDisplay::InlineGrid
)
) {
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(_)
)
}
#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss, clippy::cast_sign_loss)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] 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,
};
unsafe { crate::az_mark(0x60704_u32, (0x30u32)); }
let dom_declared = styled_dom
.css_property_cache
.ptr
.compact_cache
.as_ref()
.map_or(!0u32, |cc| cc.dom_declared_flags);
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 = 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() * super::calc::PX_PER_INCH),
SizeMetric::Cm => Some(v.number.get() * super::calc::PX_PER_INCH / super::calc::CM_PER_INCH),
SizeMetric::Mm => Some(v.number.get() * super::calc::PX_PER_INCH / super::calc::MM_PER_INCH),
_ => None, }
}
_ => None,
})
.unwrap_or(constraints.available_size.width) };
let reference_box = 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 {
LayoutTextJustify::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().copied())
.unwrap_or_default()
} else {
azul_css::props::style::text::StyleLineHeight::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 {
StyleHyphens::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 {
StyleWordBreak::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 {
StyleOverflowWrap::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 {
StyleLineBreak::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 {
StyleTextAlignLast::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_with_viewport(
&l,
0.0,
font_size,
font_size,
ctx.viewport_size.width,
ctx.viewport_size.height,
);
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().copied())
} else {
None
};
let is_intrinsic_sizing = matches!(
constraints.available_width_type,
Text3AvailableSpace::MinContent | Text3AvailableSpace::MaxContent
);
let text_indent = text_indent_prop
.map_or(0.0, |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)
});
let text_indent_each_line = text_indent_prop.is_some_and(|ti| ti.each_line);
let text_indent_hanging = text_indent_prop.is_some_and(|ti| ti.hanging);
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
| StyleWhiteSpace::PreWrap
| StyleWhiteSpace::PreLine
| StyleWhiteSpace::BreakSpaces => text3::cache::TextWrap::Wrap,
StyleWhiteSpace::Nowrap | StyleWhiteSpace::Pre => text3::cache::TextWrap::NoWrap,
};
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_or(text3::cache::InitialLetterAlign::Auto, |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,
})
} 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(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())
.is_some_and(azul_css::props::style::StyleHangingPunctuation::is_enabled)
} 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_or(0.0, |em| em.inner.get())
} 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_or(0.0, |sm| sm.inner.number.get())
} 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 | LayoutTextJustify::InterWord => {
text3::cache::JustifyContent::InterWord
}
LayoutTextJustify::InterCharacter | LayoutTextJustify::Distribute => {
text3::cache::JustifyContent::InterCharacter
}
},
line_height: if dom_declared & DOM_HAS_LINE_HEIGHT == 0 {
text3::cache::LineHeight::Normal
} else {
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(Copy, Debug, Clone)]
pub struct TableColumnInfo {
pub min_width: f32,
pub max_width: f32,
pub computed_width: Option<f32>,
}
#[derive(Copy, 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>,
col_occupied: 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(),
col_occupied: 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(Copy, Debug, Clone)]
pub struct BorderInfo {
pub width: f32,
pub style: BorderStyle,
pub color: ColorU,
pub source: BorderSource,
}
impl BorderInfo {
#[must_use] pub const fn new(width: f32, style: BorderStyle, color: ColorU, source: BorderSource) -> Self {
Self {
width,
style,
color,
source,
}
}
#[must_use] pub const 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,
}
}
#[must_use] pub fn resolve_conflict(a: &Self, b: &Self) -> Option<Self> {
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);
}
if b_is_none {
return Some(*a);
}
if a.width > b.width {
return Some(*a);
}
if b.width > a.width {
return Some(*b);
}
let a_priority = Self::style_priority(&a.style);
let b_priority = Self::style_priority(&b.style);
if a_priority > b_priority {
return Some(*a);
}
if b_priority > a_priority {
return Some(*b);
}
if a.source > b.source {
return Some(*a);
}
if b.source > a.source {
return Some(*b);
}
Some(*a)
}
}
#[allow(clippy::similar_names)] #[allow(clippy::too_many_lines)] 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,
default_border,
default_border,
default_border,
);
};
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;
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 {
f32::from(raw) / 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 }
else { BorderInfo::new(btw, bts, btc, source) };
let right = if brs == BorderStyle::None { default_border }
else { BorderInfo::new(brw, brs, brc, source) };
let bottom = if bbs == BorderStyle::None { default_border }
else { BorderInfo::new(bbw, bbs, bbc, source) };
let left = if bls == BorderStyle::None { default_border }
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_or_else(|| default_border, |style_val| {
let width = cache
.get_border_top_width(node_data, &dom_id, &node_state)
.and_then(|w| w.get_property())
.map_or(0.0, |w| {
w.inner
.resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
});
let color = cache
.get_border_top_color(node_data, &dom_id, &node_state)
.and_then(|c| c.get_property())
.map_or(ColorU {
r: 0,
g: 0,
b: 0,
a: 255,
}, |c| c.inner);
BorderInfo::new(width, style_val.inner, color, source)
});
let right = cache
.get_border_right_style(node_data, &dom_id, &node_state)
.and_then(|s| s.get_property())
.map_or_else(|| default_border, |style_val| {
let width = cache
.get_border_right_width(node_data, &dom_id, &node_state)
.and_then(|w| w.get_property())
.map_or(0.0, |w| {
w.inner
.resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
});
let color = cache
.get_border_right_color(node_data, &dom_id, &node_state)
.and_then(|c| c.get_property())
.map_or(ColorU {
r: 0,
g: 0,
b: 0,
a: 255,
}, |c| c.inner);
BorderInfo::new(width, style_val.inner, color, source)
});
let bottom = cache
.get_border_bottom_style(node_data, &dom_id, &node_state)
.and_then(|s| s.get_property())
.map_or_else(|| default_border, |style_val| {
let width = cache
.get_border_bottom_width(node_data, &dom_id, &node_state)
.and_then(|w| w.get_property())
.map_or(0.0, |w| {
w.inner
.resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
});
let color = cache
.get_border_bottom_color(node_data, &dom_id, &node_state)
.and_then(|c| c.get_property())
.map_or(ColorU {
r: 0,
g: 0,
b: 0,
a: 255,
}, |c| c.inner);
BorderInfo::new(width, style_val.inner, color, source)
});
let left = cache
.get_border_left_style(node_data, &dom_id, &node_state)
.and_then(|s| s.get_property())
.map_or_else(|| default_border, |style_val| {
let width = cache
.get_border_left_width(node_data, &dom_id, &node_state)
.and_then(|w| w.get_property())
.map_or(0.0, |w| {
w.inner
.resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
});
let color = cache
.get_border_left_color(node_data, &dom_id, &node_state)
.and_then(|c| c.get_property())
.map_or(ColorU {
r: 0,
g: 0,
b: 0,
a: 255,
}, |c| c.inner);
BorderInfo::new(width, style_val.inner, color, source)
});
(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;
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;
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(f32::from(h_raw) / 10.0),
azul_css::props::basic::pixel::PixelValue::px(f32::from(v_raw) / 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;
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;
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;
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;
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
}
#[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] pub fn layout_table_fc<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut 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(&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 (h_spacing, v_spacing) = if let Some(table_id) = tree.nodes[node_index].dom_node_id {
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);
(h_spacing, v_spacing)
} else {
(0.0f32, 0.0f32)
};
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 table_baseline = table_ctx
.row_baselines
.first()
.copied()
.and_then(|row0_baseline| {
table_ctx
.cells
.iter()
.find(|c| c.row == 0)
.and_then(|c| cell_positions.get(&c.node_index))
.map(|pos| pos.y + row0_baseline)
});
let output = LayoutOutput {
overflow_size: LogicalSize {
width: table_width,
height: total_height,
},
positions: cell_positions,
baseline: table_baseline,
};
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, &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: &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(())
}
#[allow(clippy::cast_sign_loss)] fn get_cell_spans(styled_dom: &StyledDom, dom_id: NodeId) -> (usize, usize) {
let mut colspan = 1usize;
let mut rowspan = 1usize;
let node_data = &styled_dom.node_data.as_container()[dom_id];
for attr in node_data.attributes().as_ref() {
match attr {
azul_core::dom::AttributeType::ColSpan(n) => colspan = (*n).clamp(1, 1000) as usize,
azul_core::dom::AttributeType::RowSpan(n) => rowspan = (*n).clamp(1, 65534) as usize,
_ => {}
}
}
(colspan, rowspan)
}
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, rowspan) = cell
.dom_node_id
.map_or((1, 1), |dom_id| get_cell_spans(ctx.styled_dom, dom_id));
while table_ctx
.col_occupied
.get(col_index)
.is_some_and(|&n| n > 0)
{
col_index += 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,
});
}
if rowspan > 1 {
if table_ctx.col_occupied.len() < max_col {
table_ctx.col_occupied.resize(max_col, 0);
}
for occ in &mut table_ctx.col_occupied[col_index..max_col] {
*occ = rowspan;
}
}
col_index += colspan;
}
}
}
for occ in &mut table_ctx.col_occupied {
*occ = occ.saturating_sub(1);
}
Ok(())
}
#[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_lines)] 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 Some(dom_id) = tree.get(cell_info.node_index).and_then(|n| n.dom_node_id) else {
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,
get_element_font_size(ctx.styled_dom, dom_id, node_state),
get_root_font_size(ctx.styled_dom, node_state),
)
}
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 TextLayoutCache,
cell_index: usize,
constraints: &LayoutConstraints<'_>,
sizing_mode: text3::cache::AvailableSpace,
) -> Result<f32> {
let width_type = match sizing_mode {
text3::cache::AvailableSpace::MinContent => Text3AvailableSpace::MinContent,
text3::cache::AvailableSpace::MaxContent => Text3AvailableSpace::MaxContent,
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_or_else(|| {
tree.get(cell_index)
.and_then(|n| n.used_size)
.map_or(0.0, |s| s.width)
}, |s| s.width);
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 TextLayoutCache,
cell_index: usize,
constraints: &LayoutConstraints<'_>,
) -> Result<f32> {
measure_cell_content_width(
ctx, tree, text_cache, cell_index, constraints,
text3::cache::AvailableSpace::MinContent,
)
}
fn measure_cell_max_content_width<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut TextLayoutCache,
cell_index: usize,
constraints: &LayoutConstraints<'_>,
) -> Result<f32> {
measure_cell_content_width(
ctx, tree, text_cache, cell_index, constraints,
text3::cache::AvailableSpace::MaxContent,
)
}
fn calculate_column_widths_auto<T: ParsedFontTrait>(
table_ctx: &mut TableLayoutContext,
tree: &mut LayoutTree,
text_cache: &mut 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,
)
}
#[allow(clippy::suboptimal_flops)] #[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_lines)] fn calculate_column_widths_auto_with_width<T: ParsedFontTrait>(
table_ctx: &mut TableLayoutContext,
tree: &mut LayoutTree,
text_cache: &mut 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 {
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);
}
}
}
Ok(())
}
#[allow(clippy::cast_precision_loss)] 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;
}
}
}
}
#[allow(clippy::too_many_lines)] fn layout_cell_for_height<T: ParsedFontTrait>(
ctx: &mut LayoutContext<'_, T>,
tree: &mut LayoutTree,
text_cache: &mut 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, _) = 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
}
#[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn calculate_row_heights<T: ParsedFontTrait>(
table_ctx: &mut TableLayoutContext,
tree: &mut LayoutTree,
text_cache: &mut 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).min(table_ctx.row_heights.len());
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| {
tree.get(cell_idx).is_none_or(|cell_node| {
let ec = get_empty_cells_property(ctx, cell_node);
ec == StyleEmptyCells::Hide && is_cell_empty(tree, cell_idx)
})
});
if all_hidden_empty {
table_ctx.row_heights[row_idx] = 0.0;
table_ctx.hidden_empty_rows.insert(row_idx);
}
}
}
Ok(())
}
#[allow(clippy::suboptimal_flops)] #[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] fn position_table_cells<T: ParsedFontTrait>(
table_ctx: &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;
if let Some(table_id) = tree.nodes[table_index].dom_node_id {
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)
}
} 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;
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;
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: 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(),
};
let mut cil = CachedInlineLayout::new(
Arc::new(adjusted_layout),
available_width,
has_floats,
);
cil.inline_content_hash = cached_layout.inline_content_hash;
warm_mut.inline_layout_result = Some(cil);
}
}
}
let cell_has_inline = tree
.warm(cell_info.node_index)
.is_some_and(|w| w.inline_layout_result.is_some());
if !cell_has_inline {
let vertical_align = cell_dom_node_id.map_or(StyleVerticalAlign::Baseline, |dom_id| {
let node_state =
ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
match get_vertical_align_property(ctx.styled_dom, dom_id, &node_state) {
MultiValue::Exact(v) => v,
_ => StyleVerticalAlign::Baseline,
}
});
let factor = match vertical_align {
StyleVerticalAlign::Middle => 0.5,
StyleVerticalAlign::Bottom => 1.0,
_ => 0.0,
};
if factor > 0.0 {
let children: Vec<usize> = tree.children(cell_info.node_index).to_vec();
let mut content_height = 0.0f32;
let mut inflow: Vec<usize> = Vec::new();
for &c in &children {
let dom_id = tree.get(c).and_then(|n| n.dom_node_id);
if matches!(
get_position_type(ctx.styled_dom, dom_id),
LayoutPosition::Absolute | LayoutPosition::Fixed
) {
continue; }
let top = tree
.warm(c)
.and_then(|w| w.relative_position)
.map_or(0.0, |p| p.y);
let h = tree.get(c).and_then(|n| n.used_size).map_or(0.0, |s| s.height);
content_height = content_height.max(top + h);
inflow.push(c);
}
let content_box_height = height
- cell_box_props.padding.main_start(writing_mode)
- cell_box_props.padding.main_end(writing_mode)
- cell_box_props.border.main_start(writing_mode)
- cell_box_props.border.main_end(writing_mode);
let y_offset = (content_box_height - content_height) * factor;
if y_offset > 0.01 {
for &c in &inflow {
if let Some(w) = tree.warm_mut(c) {
if let Some(pos) = w.relative_position.as_mut() {
pos.y += y_offset;
}
}
}
}
}
}
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 mut content = Vec::new();
let mut child_map = HashMap::new();
collect_and_measure_inline_content_impl(
ctx,
text_cache,
tree,
ifc_root_index,
constraints,
&mut content,
&mut child_map,
)?;
Ok((content, child_map))
}
#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] 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<'_>,
content: &mut Vec<InlineContent>,
child_map: &mut HashMap<ContentIndex, usize>,
) -> Result<()> {
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 current_run_index: u32 = 0;
#[cfg(feature = "web_lift")]
unsafe {
crate::az_mark((0x60690) as u32, (content as *const _ as usize as u32) as u32);
crate::az_mark((0x60694) as u32, (ifc_root_index as u32 | 0xC0DE0000u32) as u32);
crate::az_mark((0x606A8) as u32, (tree.nodes.len() as u32) as u32);
crate::az_mark((0x606AC) as u32, (tree.get(ifc_root_index).is_some() as u32 | 0xC0DE0000u32) as u32);
crate::az_mark((0x606B0) as u32, (tree.root as u32) as u32);
let slot = (ifc_root_index & 7) * 4;
crate::az_mark(((0x60940 + slot)) as u32, (tree.nodes.len() as u32) as u32);
crate::az_mark(((0x60960 + slot)) as u32, ((&*tree as *const LayoutTree as usize) as u32) as u32);
}
let ifc_root_node = tree.get(ifc_root_index).ok_or(LayoutError::InvalidTree)?;
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark((0x606A4) as u32, (0x0000_6449u32) as u32); }
let is_anonymous = ifc_root_node.dom_node_id.is_none();
let ifc_root_dom_id = if let Some(id) = ifc_root_node.dom_node_id { id } else {
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 {
if let Some(id) = tree.children(ifc_root_index)
.iter()
.filter_map(|&child_idx| tree.get(child_idx)).find_map(|n| n.dom_node_id) { id } else {
debug_warning!(ctx, "IFC root and all ancestors/children have no DOM ID");
return Ok(());
}
}
};
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;
}
if matches!(node_data.get_node_type(), NodeType::Br) {
content.push(InlineContent::LineBreak(InlineBreak {
break_type: BreakType::Hard,
clear: ClearType::None,
content_index: content.len(),
}));
continue;
}
if matches!(
get_position_type(ctx.styled_dom, Some(dom_id)),
LayoutPosition::Absolute | LayoutPosition::Fixed
) {
continue;
}
let display = get_display_property(ctx.styled_dom, Some(dom_id)).unwrap_or_default();
if display == LayoutDisplay::Inline {
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,
content,
&children,
constraints,
)?;
} else {
let intrinsic_size = tree.warm(child_index).and_then(|w| w.intrinsic_sizes).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)
.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 is_replaced_atomic = {
let nd = &ctx.styled_dom.node_data.as_container()[dom_id];
matches!(nd.get_node_type(), NodeType::Image(_)) || nd.is_virtual_view_node()
};
let final_height = match css_height.unwrap_or_default() {
LayoutHeight::Auto if !is_replaced_atomic => {
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);
}
}
return Ok(());
}
let ifc_root_node = tree.get(ifc_root_index).ok_or(LayoutError::InvalidTree)?;
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark((0x606A4) as u32, (0x0000_6706u32) as u32); }
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)
.is_some_and(|w| w.pseudo_element == Some(PseudoElement::Marker))
})
.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 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(());
}
let _ifc_root_node_type = match ifc_root_node_data.get_node_type() {
NodeType::Div => "Div",
NodeType::Text(_) => "Text",
NodeType::Body => "Body",
_ => "Other",
};
let dom_children: Vec<NodeId> = ifc_root_dom_id
.az_children(&ctx.styled_dom.node_hierarchy.as_container())
.collect();
#[cfg(feature = "web_lift")]
let dom_children_len = unsafe {
crate::az_mark((0x606B4) as u32, (dom_children.len() as u32) as u32);
crate::az_mark((0x606A4) as u32, (0x0000_6863u32) as u32);
crate::az_mark_read(0x606B4) as usize
};
#[cfg(not(feature = "web_lift"))]
let dom_children_len = dom_children.len();
for item_idx in 0..dom_children_len {
let dom_child_id = unsafe { *dom_children.get_unchecked(item_idx) };
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];
#[cfg(feature = "web_lift")]
unsafe {
if item_idx == 0 {
crate::az_mark((0x606B8) as u32, (match node_data.get_node_type() {
NodeType::Text(_) => 0xC0DE_7E70u32,
NodeType::Div => 0xC0DE_D11Fu32,
NodeType::Body => 0xC0DE_B0D1u32,
_ => 0xC0DE_0000u32,
}) as u32);
}
crate::az_mark((0x606A4) as u32, (0x0000_6896u32) as u32);
}
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);
#[cfg(feature = "web_lift")]
unsafe {
crate::az_mark((0x606A4) as u32, (0x0000_6905u32) as u32);
crate::az_mark((0x606BC) as u32, (content.len() as u32) as u32);
}
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;
}
if matches!(node_data.get_node_type(), NodeType::Br) {
content.push(InlineContent::LineBreak(InlineBreak {
break_type: BreakType::Hard,
clear: ClearType::None,
content_index: content.len(),
}));
continue;
}
let child_index = children
.iter()
.find(|&&idx| {
tree.get(idx)
.and_then(|n| n.dom_node_id)
.is_some_and(|id| id == dom_child_id)
})
.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;
};
#[cfg(feature = "web_lift")]
unsafe { crate::az_mark((0x606A4) as u32, (0x0000_6942u32) as u32); }
let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
let dom_id = child_node.dom_node_id.unwrap();
if matches!(
get_position_type(ctx.styled_dom, Some(dom_id)),
LayoutPosition::Absolute | LayoutPosition::Fixed
) {
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).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)
.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 is_replaced_atomic = {
let nd = &ctx.styled_dom.node_data.as_container()[dom_id];
matches!(nd.get_node_type(), NodeType::Image(_)) || nd.is_virtual_view_node()
};
let final_height = match css_height.clone().unwrap_or_default() {
LayoutHeight::Auto if !is_replaced_atomic => {
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)
.unwrap_or_else(|| 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)
.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,
)?;
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: 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,
content,
&children,
constraints,
)?;
}
}
#[cfg(feature = "web_lift")]
unsafe {
crate::az_mark((0x60698) as u32, (content.len() as u32) as u32);
crate::az_mark((0x6069C) as u32, (0xC0DE069Cu32) as u32);
}
Ok(())
}
#[allow(clippy::too_many_lines)] 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>,
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_or(text3::cache::LineHeight::Normal, |v| {
let n = v.inner.normalized();
let px = if n < 0.0 { -n } else { n * font_size };
text3::cache::LineHeight::Px(px)
});
let cb_width = constraints.containing_block_size.main(constraints.writing_mode);
let padding_top = get_css_padding_top(ctx.styled_dom, span_dom_id, node_state)
.exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
let padding_bottom = get_css_padding_bottom(ctx.styled_dom, span_dom_id, node_state)
.exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
let padding_left = crate::solver3::getters::get_css_padding_left(ctx.styled_dom, span_dom_id, node_state)
.exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
let padding_right = crate::solver3::getters::get_css_padding_right(ctx.styled_dom, span_dom_id, node_state)
.exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
let border_top = get_css_border_top_width(ctx.styled_dom, span_dom_id, node_state)
.exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
let border_bottom = get_css_border_bottom_width(ctx.styled_dom, span_dom_id, node_state)
.exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
let border_left = crate::solver3::getters::get_css_border_left_width(ctx.styled_dom, span_dom_id, node_state)
.exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
let border_right = crate::solver3::getters::get_css_border_right_width(ctx.styled_dom, span_dom_id, node_state)
.exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
let margin_left = crate::solver3::getters::get_css_margin_left(ctx.styled_dom, span_dom_id, node_state)
.exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
let margin_right = crate::solver3::getters::get_css_margin_right(ctx.styled_dom, span_dom_id, node_state)
.exact().map_or(0.0, |pv| pv.to_pixels_internal(cb_width, font_size, DEFAULT_FONT_SIZE));
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;
}
if matches!(node_data.get_node_type(), NodeType::Br) {
content.push(InlineContent::LineBreak(InlineBreak {
break_type: BreakType::Hard,
clear: ClearType::None,
content_index: content.len(),
}));
continue;
}
if matches!(
get_position_type(ctx.styled_dom, Some(child_dom_id)),
LayoutPosition::Absolute | LayoutPosition::Fixed
) {
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)
.is_some_and(|id| id == child_dom_id)
})
.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,
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).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)
.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,
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);
}
{
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)
}
#[must_use] 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
&& content_size.width > (container_size.width - scrollbar_width_px) + EPSILON {
needs_horizontal = true;
}
if needs_horizontal && !needs_vertical && overflow_y == OverflowBehavior::Auto
&& 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,
}
}
#[must_use] 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 Some(node) = tree.get(node_index) else {
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 Some(marker_node) = tree.get(marker_index) else {
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 {marker_index} is not a ::marker pseudo-element \
(pseudo={marker_pseudo:?}, anonymous_type={marker_anonymous_type:?})"
)));
}
if marker_anonymous_type != Some(AnonymousBoxType::ListItemMarker) {
return String::new();
}
}
let Some(list_item_index) = marker_node.parent else {
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 Some(list_item_node) = tree.get(list_item_index) else {
return String::new();
};
let Some(list_item_dom_id) = list_item_node.dom_node_id else {
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={marker_index}, list_item_index={list_item_index}, \
list_item_dom_id={list_item_dom_id:?}"
)));
}
let list_container_dom_id = list_item_node.parent.and_then(|grandparent_index| {
tree.get(grandparent_index).and_then(|grandparent| grandparent.dom_node_id)
});
let list_style_type = list_container_dom_id.map_or_else(|| get_list_style_type(styled_dom, Some(list_item_dom_id)), |container_id| {
let container_type = get_list_style_type(styled_dom, Some(container_id));
if container_type == StyleListStyleType::default() {
get_list_style_type(styled_dom, Some(list_item_dom_id))
} else {
container_type
}
});
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 \
{list_item_index}, defaulting to 1"
)));
}
1
});
if let Some(msgs) = debug_messages {
msgs.push(LayoutDebugMessage::info(format!(
"[generate_list_marker_text] counter_value={counter_value} for list_item_index={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 {
text3::cache::FontStack::Stack(selectors) => {
selectors.iter().map(|f| f.family.as_str()).collect()
}
text3::cache::FontStack::Ref(_) => vec!["<embedded-font>"],
};
msgs.push(LayoutDebugMessage::info(format!(
"[generate_list_marker_segments] Marker text: '{marker_text}' with font stack: {font_families:?}"
)));
}
vec![StyledRun {
text: marker_text,
style: base_style,
logical_start_byte: 0,
source_node_id: None,
}]
}
#[inline]
const 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
}
const 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]
const fn is_css_document_whitespace(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')
}
#[allow(clippy::too_many_lines)] pub fn split_text_for_whitespace(
styled_dom: &StyledDom,
dom_id: NodeId,
text: &str,
style: &Arc<StyleProperties>,
) -> Vec<InlineContent> {
let text_owned;
let text: &str = if text.chars().any(is_bidi_control) {
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 = parent_id.map_or(StyleWhiteSpace::Normal, |parent| {
let styled_nodes = styled_dom.styled_nodes.as_container();
let parent_state = styled_nodes
.get(parent)
.map(|n| n.styled_node_state)
.unwrap_or_default();
match get_white_space_property(styled_dom, parent, &parent_state) {
MultiValue::Exact(ws) => ws,
_ => 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().is_some_and(is_css_document_whitespace);
let had_trailing = segment.chars().last().is_some_and(is_css_document_whitespace);
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 != text3::cache::TextTransform::None {
for item in &mut result {
if let InlineContent::Text(run) = item {
run.text = apply_text_transform(&run.text, text_transform);
}
}
}
result
}
fn apply_text_transform(text: &str, transform: 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()
}
}
}
#[allow(clippy::cast_precision_loss)] #[must_use] pub fn layout_initial_letter(
initial_letter_size: f32,
initial_letter_sink: u32,
content_box_width: f32,
line_height: f32,
) -> (f32, f32) {
const CAP_WIDTH_RATIO: f32 = 0.7;
const LETTER_GAP: f32 = 4.0;
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;
let letter_width_raw = letter_height * CAP_WIDTH_RATIO;
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)
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::too_many_lines)]
mod autotest_generated {
use azul_core::dom::{AttributeType, Dom, IdOrClass};
use super::*;
use crate::{
solver3::geometry::{MarginAuto, PackedBoxProps},
text3::cache::{OverflowInfo, TextTransform, UnifiedLayout},
};
const HTB: LayoutWritingMode = LayoutWritingMode::HorizontalTb;
const VRL: LayoutWritingMode = LayoutWritingMode::VerticalRl;
fn size(w: f32, h: f32) -> LogicalSize {
LogicalSize::new(w, h)
}
fn edges(top: f32, right: f32, bottom: f32, left: f32) -> EdgeSizes {
EdgeSizes {
top,
right,
bottom,
left,
}
}
fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
LogicalRect::new(LogicalPosition::new(x, y), size(w, h))
}
fn box_props(margin: EdgeSizes, border: EdgeSizes, padding: EdgeSizes) -> BoxProps {
BoxProps {
margin,
padding,
border,
margin_auto: MarginAuto::default(),
}
}
fn hot(
parent: Option<usize>,
used_size: Option<LogicalSize>,
bp: &BoxProps,
) -> LayoutNodeHot {
LayoutNodeHot {
box_props: PackedBoxProps::pack(bp),
dom_node_id: None,
used_size,
formatting_context: FormattingContext::Block {
establishes_new_context: false,
},
parent,
}
}
fn build_tree(
nodes: Vec<LayoutNodeHot>,
warm: Vec<LayoutNodeWarm>,
child_lists: &[Vec<usize>],
) -> LayoutTree {
let n = nodes.len();
let mut children_arena: Vec<usize> = Vec::new();
let mut children_offsets: Vec<(u32, u32)> = Vec::with_capacity(n);
for cl in child_lists {
let start = u32::try_from(children_arena.len()).unwrap();
children_arena.extend_from_slice(cl);
children_offsets.push((start, u32::try_from(cl.len()).unwrap()));
}
while children_offsets.len() < n {
children_offsets.push((0, 0));
}
LayoutTree {
nodes,
warm,
cold: vec![LayoutNodeCold::default(); n],
root: 0,
dom_to_layout: BTreeMap::new(),
children_arena,
children_offsets,
subtree_needs_intrinsic: Vec::new(),
}
}
fn empty_inline_layout() -> CachedInlineLayout {
CachedInlineLayout::new(
Arc::new(UnifiedLayout {
items: Vec::new(),
overflow: OverflowInfo::default(),
}),
Text3AvailableSpace::MaxContent,
false,
)
}
fn constraints(available: LogicalSize, wm: LayoutWritingMode) -> LayoutConstraints<'static> {
LayoutConstraints {
available_size: available,
writing_mode: wm,
writing_mode_ctx: super::super::geometry::WritingModeContext::default(),
bfc_state: None,
text_align: TextAlign::Start,
containing_block_size: available,
available_width_type: Text3AvailableSpace::Definite(available.width),
}
}
fn styled(dom: Dom, css_str: &str) -> StyledDom {
let mut dom = dom;
let (css, _warnings) = azul_css::parser2::new_from_str(css_str);
StyledDom::create(&mut dom, css)
}
fn text_dom(text: &str, css_str: &str) -> StyledDom {
styled(
Dom::create_body().with_child(
Dom::create_div()
.with_ids_and_classes(vec![IdOrClass::Class("p".into())].into())
.with_child(Dom::create_text(text)),
),
css_str,
)
}
const TEXT_NODE: NodeId = NodeId::new(2);
const DIV_NODE: NodeId = NodeId::new(1);
fn plain_style() -> Arc<StyleProperties> {
Arc::new(StyleProperties::default())
}
fn text_of(item: &InlineContent) -> Option<&str> {
match item {
InlineContent::Text(run) => Some(run.text.as_str()),
_ => None,
}
}
const ALL_OVERFLOW: [OverflowBehavior; 5] = [
OverflowBehavior::Visible,
OverflowBehavior::Hidden,
OverflowBehavior::Clip,
OverflowBehavior::Scroll,
OverflowBehavior::Auto,
];
#[test]
fn overflow_behavior_is_clipped_is_exhaustive_and_total() {
assert!(!OverflowBehavior::Visible.is_clipped());
assert!(OverflowBehavior::Hidden.is_clipped());
assert!(OverflowBehavior::Clip.is_clipped());
assert!(OverflowBehavior::Scroll.is_clipped());
assert!(OverflowBehavior::Auto.is_clipped());
assert_eq!(ALL_OVERFLOW.iter().filter(|o| o.is_clipped()).count(), 4);
}
#[test]
fn overflow_behavior_is_scroll_only_for_scroll_and_auto() {
assert!(!OverflowBehavior::Visible.is_scroll());
assert!(!OverflowBehavior::Hidden.is_scroll());
assert!(!OverflowBehavior::Clip.is_scroll());
assert!(OverflowBehavior::Scroll.is_scroll());
assert!(OverflowBehavior::Auto.is_scroll());
}
#[test]
fn overflow_behavior_scroll_implies_clipped_invariant() {
for o in ALL_OVERFLOW {
assert!(
!o.is_scroll() || o.is_clipped(),
"{o:?} is scroll but not clipped"
);
}
}
#[test]
fn bfc_layout_result_from_output_preserves_output_and_nulls_escaped_margins() {
let mut positions = BTreeMap::new();
positions.insert(usize::MAX, LogicalPosition::new(f32::MIN, f32::MAX));
let output = LayoutOutput {
positions,
overflow_size: size(f32::NAN, f32::INFINITY),
baseline: Some(-0.0),
};
let res = BfcLayoutResult::from_output(output);
assert!(res.escaped_top_margin.is_none());
assert!(res.escaped_bottom_margin.is_none());
assert_eq!(res.output.positions.len(), 1);
assert!(res.output.overflow_size.width.is_nan());
assert!(res.output.overflow_size.height.is_infinite());
assert_eq!(res.output.baseline, Some(-0.0));
}
#[test]
fn bfc_state_new_matches_default_and_starts_empty() {
let s = BfcState::new();
assert_eq!(s.pen, LogicalPosition::zero());
assert!(s.floats.floats.is_empty());
assert_eq!(s.margins.last_in_flow_margin_bottom, 0.0);
let d = BfcState::default();
assert_eq!(d.pen, s.pen);
assert!(d.floats.floats.is_empty());
}
#[test]
fn table_layout_context_new_starts_empty_and_separate() {
let t = TableLayoutContext::new();
assert!(t.columns.is_empty());
assert!(t.cells.is_empty());
assert_eq!(t.num_rows, 0);
assert!(!t.use_fixed_layout);
assert!(t.row_heights.is_empty());
assert!(t.row_baselines.is_empty());
assert!(matches!(t.border_collapse, StyleBorderCollapse::Separate));
assert!(t.caption_index.is_none());
assert!(t.collapsed_rows.is_empty());
assert!(t.collapsed_columns.is_empty());
assert!(t.hidden_empty_rows.is_empty());
assert!(t.row_node_indices.is_empty());
}
#[test]
fn add_float_accepts_extreme_geometry_without_panicking() {
let mut fc = FloatingContext::default();
fc.add_float(LayoutFloat::Left, rect(0.0, 0.0, 0.0, 0.0), EdgeSizes::default());
fc.add_float(
LayoutFloat::Right,
rect(f32::MIN, f32::MIN, f32::MAX, f32::MAX),
edges(f32::MAX, f32::MAX, f32::MAX, f32::MAX),
);
fc.add_float(
LayoutFloat::None,
rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
edges(f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.0),
);
assert_eq!(fc.floats.len(), 3);
assert!(fc.floats[2].rect.size.width.is_nan());
}
#[test]
fn available_line_box_space_with_no_floats_returns_the_full_band() {
let fc = FloatingContext::default();
assert_eq!(fc.available_line_box_space(0.0, 0.0, 0.0, HTB), (0.0, 0.0));
assert_eq!(
fc.available_line_box_space(0.0, 20.0, 300.0, HTB),
(0.0, 300.0)
);
assert_eq!(
fc.available_line_box_space(-50.0, -10.0, -5.0, HTB),
(0.0, -5.0)
);
let (s, e) = fc.available_line_box_space(f32::MIN, f32::MAX, f32::MAX, HTB);
assert_eq!(s, 0.0);
assert_eq!(e, f32::MAX);
}
#[test]
fn available_line_box_space_subtracts_left_and_right_float_margin_boxes() {
let mut fc = FloatingContext::default();
fc.add_float(
LayoutFloat::Left,
rect(10.0, 10.0, 100.0, 50.0),
edges(10.0, 10.0, 10.0, 10.0),
);
fc.add_float(
LayoutFloat::Right,
rect(200.0, 10.0, 90.0, 50.0),
edges(10.0, 10.0, 10.0, 10.0),
);
let (start, end) = fc.available_line_box_space(10.0, 20.0, 300.0, HTB);
assert_eq!(start, 120.0); assert_eq!(end, 190.0);
assert_eq!(
fc.available_line_box_space(1000.0, 1010.0, 300.0, HTB),
(0.0, 300.0)
);
}
#[test]
fn available_line_box_space_main_axis_overlap_is_half_open() {
let mut fc = FloatingContext::default();
fc.add_float(
LayoutFloat::Left,
rect(0.0, 0.0, 100.0, 50.0),
EdgeSizes::default(),
);
assert_eq!(
fc.available_line_box_space(50.0, 70.0, 300.0, HTB),
(0.0, 300.0)
);
assert_eq!(
fc.available_line_box_space(0.0, 0.0, 300.0, HTB),
(0.0, 300.0)
);
assert_eq!(
fc.available_line_box_space(49.0, 70.0, 300.0, HTB),
(100.0, 300.0)
);
}
#[test]
fn available_line_box_space_ignores_nan_geometry_instead_of_panicking() {
let mut fc = FloatingContext::default();
fc.add_float(
LayoutFloat::Left,
rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
EdgeSizes::default(),
);
assert_eq!(
fc.available_line_box_space(0.0, 20.0, 300.0, HTB),
(0.0, 300.0)
);
let (s, e) = fc.available_line_box_space(f32::NAN, f32::NAN, 300.0, HTB);
assert_eq!((s, e), (0.0, 300.0));
}
#[test]
fn available_line_box_space_is_writing_mode_aware() {
let mut fc = FloatingContext::default();
fc.add_float(
LayoutFloat::Left,
rect(0.0, 0.0, 50.0, 100.0),
EdgeSizes::default(),
);
assert_eq!(
fc.available_line_box_space(10.0, 20.0, 300.0, VRL),
(100.0, 300.0)
);
assert_eq!(
fc.available_line_box_space(10.0, 20.0, 300.0, HTB),
(50.0, 300.0)
);
}
#[test]
fn clearance_offset_never_moves_content_upwards() {
let mut fc = FloatingContext::default();
fc.add_float(
LayoutFloat::Left,
rect(0.0, 0.0, 100.0, 80.0),
edges(0.0, 0.0, 20.0, 0.0), );
fc.add_float(
LayoutFloat::Right,
rect(200.0, 0.0, 100.0, 40.0),
EdgeSizes::default(),
);
assert_eq!(fc.clearance_offset(LayoutClear::Left, 0.0, HTB), 100.0);
assert_eq!(fc.clearance_offset(LayoutClear::Right, 0.0, HTB), 40.0);
assert_eq!(fc.clearance_offset(LayoutClear::Both, 0.0, HTB), 100.0);
assert_eq!(fc.clearance_offset(LayoutClear::None, 25.0, HTB), 25.0);
assert_eq!(fc.clearance_offset(LayoutClear::Both, 500.0, HTB), 500.0);
}
#[test]
fn clearance_offset_clamps_negative_offsets_to_zero() {
let fc = FloatingContext::default();
assert_eq!(fc.clearance_offset(LayoutClear::Both, -10.0, HTB), 0.0);
assert_eq!(fc.clearance_offset(LayoutClear::None, -10.0, HTB), 0.0);
assert_eq!(fc.clearance_offset(LayoutClear::None, 0.0, HTB), 0.0);
}
#[test]
fn clearance_offset_handles_nan_and_infinite_floats() {
let mut fc = FloatingContext::default();
fc.add_float(
LayoutFloat::Left,
rect(0.0, 0.0, 10.0, f32::INFINITY),
EdgeSizes::default(),
);
assert!(fc.clearance_offset(LayoutClear::Left, 0.0, HTB).is_infinite());
assert!(fc.clearance_offset(LayoutClear::Left, f32::NAN, HTB).is_nan());
let mut nan_fc = FloatingContext::default();
nan_fc.add_float(
LayoutFloat::Left,
rect(f32::NAN, f32::NAN, f32::NAN, f32::NAN),
EdgeSizes::default(),
);
assert_eq!(nan_fc.clearance_offset(LayoutClear::Left, 5.0, HTB), 5.0);
}
#[test]
fn clearance_offset_saturates_at_float_extremes() {
let mut fc = FloatingContext::default();
fc.add_float(
LayoutFloat::Right,
rect(f32::MAX, 0.0, f32::MAX, f32::MAX),
edges(0.0, 0.0, f32::MAX, 0.0),
);
let out = fc.clearance_offset(LayoutClear::Right, f32::MIN, HTB);
assert!(out.is_infinite() && out.is_sign_positive());
}
#[test]
fn position_float_places_left_and_right_floats_at_the_band_edges() {
let fc = FloatingContext::default();
let m = edges(10.0, 10.0, 10.0, 10.0);
let left = position_float(&fc, LayoutFloat::Left, size(100.0, 50.0), &m, 0.0, 300.0, HTB);
assert_eq!(left.origin.x, 10.0); assert_eq!(left.origin.y, 10.0); assert_eq!(left.size, size(100.0, 50.0));
let right = position_float(&fc, LayoutFloat::Right, size(100.0, 50.0), &m, 0.0, 300.0, HTB);
assert_eq!(right.origin.x, 190.0);
assert_eq!(right.origin.y, 10.0);
}
#[test]
fn position_float_at_zero_size_and_zero_band() {
let fc = FloatingContext::default();
let r = position_float(
&fc,
LayoutFloat::Left,
size(0.0, 0.0),
&EdgeSizes::default(),
0.0,
0.0,
HTB,
);
assert_eq!(r.origin, LogicalPosition::zero());
assert_eq!(r.size, size(0.0, 0.0));
}
#[test]
fn position_float_wider_than_the_bfc_does_not_hang() {
let fc = FloatingContext::default();
let left = position_float(
&fc,
LayoutFloat::Left,
size(500.0, 50.0),
&EdgeSizes::default(),
0.0,
300.0,
HTB,
);
assert_eq!(left.origin.x, 0.0);
let right = position_float(
&fc,
LayoutFloat::Right,
size(500.0, 50.0),
&EdgeSizes::default(),
0.0,
300.0,
HTB,
);
assert_eq!(right.origin.x, -200.0);
}
#[test]
fn position_float_stacks_then_shifts_down_past_the_lowest_float() {
let mut fc = FloatingContext::default();
fc.add_float(
LayoutFloat::Left,
rect(0.0, 0.0, 100.0, 50.0),
EdgeSizes::default(),
);
let beside = position_float(
&fc,
LayoutFloat::Left,
size(100.0, 50.0),
&EdgeSizes::default(),
0.0,
300.0,
HTB,
);
assert_eq!(beside.origin.x, 100.0);
assert_eq!(beside.origin.y, 0.0);
let below = position_float(
&fc,
LayoutFloat::Left,
size(250.0, 50.0),
&EdgeSizes::default(),
0.0,
300.0,
HTB,
);
assert_eq!(below.origin.x, 0.0);
assert_eq!(below.origin.y, 50.0);
}
#[test]
fn position_float_terminates_on_nan_and_infinite_sizes() {
let mut fc = FloatingContext::default();
fc.add_float(
LayoutFloat::Left,
rect(0.0, 0.0, 100.0, 50.0),
EdgeSizes::default(),
);
let nan = position_float(
&fc,
LayoutFloat::Left,
size(f32::NAN, f32::NAN),
&EdgeSizes::default(),
0.0,
300.0,
HTB,
);
assert!(nan.size.width.is_nan());
assert!(nan.origin.x.is_finite());
let inf = position_float(
&fc,
LayoutFloat::Left,
size(f32::INFINITY, f32::INFINITY),
&EdgeSizes::default(),
0.0,
300.0,
HTB,
);
assert_eq!(inf.origin.x, 0.0);
}
#[test]
fn position_float_honours_vertical_writing_mode() {
let fc = FloatingContext::default();
let r = position_float(
&fc,
LayoutFloat::Left,
size(50.0, 100.0),
&EdgeSizes::default(),
20.0,
300.0,
VRL,
);
assert_eq!(r.origin.x, 20.0); assert_eq!(r.origin.y, 0.0); }
#[test]
fn position_floated_child_rejects_float_none() {
let mut fc = FloatingContext::default();
let c = constraints(size(300.0, 300.0), HTB);
let out = position_floated_child(
0,
size(10.0, 10.0),
LayoutFloat::None,
&c,
rect(0.0, 0.0, 300.0, 300.0),
0.0,
&mut fc,
);
assert!(matches!(out, Err(LayoutError::PositioningFailed)));
assert!(fc.floats.is_empty());
}
#[test]
fn position_floated_child_places_and_records_the_float() {
let mut fc = FloatingContext::default();
let c = constraints(size(300.0, 300.0), HTB);
let left = position_floated_child(
0,
size(100.0, 50.0),
LayoutFloat::Left,
&c,
rect(0.0, 0.0, 300.0, 300.0),
0.0,
&mut fc,
)
.expect("fits");
assert_eq!(left, LogicalPosition::new(0.0, 0.0));
assert_eq!(fc.floats.len(), 1);
let right = position_floated_child(
1,
size(100.0, 50.0),
LayoutFloat::Right,
&c,
rect(0.0, 0.0, 300.0, 300.0),
0.0,
&mut fc,
)
.expect("fits");
assert_eq!(right, LogicalPosition::new(200.0, 0.0));
assert_eq!(fc.floats.len(), 2);
let third = position_floated_child(
2,
size(150.0, 50.0),
LayoutFloat::Left,
&c,
rect(0.0, 0.0, 300.0, 300.0),
0.0,
&mut fc,
)
.expect("drops to the next band");
assert_eq!(third, LogicalPosition::new(0.0, 50.0));
}
#[test]
fn position_floated_child_errors_instead_of_looping_when_nothing_can_fit() {
let mut fc = FloatingContext::default();
let c = constraints(size(300.0, 300.0), HTB);
let out = position_floated_child(
0,
size(500.0, 50.0),
LayoutFloat::Left,
&c,
rect(0.0, 0.0, 300.0, 300.0),
0.0,
&mut fc,
);
assert!(matches!(out, Err(LayoutError::PositioningFailed)));
assert!(fc.floats.is_empty());
let nan = position_floated_child(
0,
size(f32::NAN, f32::NAN),
LayoutFloat::Left,
&c,
rect(0.0, 0.0, 300.0, 300.0),
0.0,
&mut fc,
);
assert!(matches!(nan, Err(LayoutError::PositioningFailed)));
}
#[test]
fn translate_taffy_size_round_trips_through_taffy() {
for s in [
size(0.0, 0.0),
size(-0.0, 1.5),
size(f32::MIN, f32::MAX),
size(-1.0, -2.0),
size(f32::INFINITY, f32::NEG_INFINITY),
size(f32::MIN_POSITIVE, f32::EPSILON),
] {
let t = translate_taffy_size(s);
let back = translate_taffy_size_back(TaffySize {
width: t.width.unwrap(),
height: t.height.unwrap(),
});
assert_eq!(back.width.to_bits(), s.width.to_bits(), "width for {s:?}");
assert_eq!(back.height.to_bits(), s.height.to_bits(), "height for {s:?}");
}
}
#[test]
fn translate_taffy_size_preserves_nan_without_panicking() {
let t = translate_taffy_size(size(f32::NAN, f32::NAN));
assert!(t.width.unwrap().is_nan());
let back = translate_taffy_size_back(TaffySize {
width: t.width.unwrap(),
height: t.height.unwrap(),
});
assert!(back.width.is_nan() && back.height.is_nan());
}
#[test]
fn translate_taffy_point_back_is_a_field_for_field_copy() {
for (x, y) in [
(0.0_f32, 0.0_f32),
(f32::MIN, f32::MAX),
(-1.5, 2.5),
(f32::INFINITY, f32::NEG_INFINITY),
] {
let p = translate_taffy_point_back(taffy::Point { x, y });
assert_eq!(p.x.to_bits(), x.to_bits());
assert_eq!(p.y.to_bits(), y.to_bits());
}
let nan = translate_taffy_point_back(taffy::Point {
x: f32::NAN,
y: f32::NAN,
});
assert!(nan.x.is_nan() && nan.y.is_nan());
}
fn resolve(metric: SizeMetric, value: f32) -> f32 {
resolve_size_metric(metric, value, 200.0, size(1000.0, 500.0), 16.0, 10.0)
}
fn approx(actual: f32, expected: f32) {
assert!(
(actual - expected).abs() < 0.001,
"expected ~{expected}, got {actual}"
);
}
#[test]
fn resolve_size_metric_converts_every_unit() {
assert_eq!(resolve(SizeMetric::Px, 42.0), 42.0);
approx(resolve(SizeMetric::Pt, 72.0), 96.0); assert_eq!(resolve(SizeMetric::Percent, 50.0), 100.0); assert_eq!(resolve(SizeMetric::Em, 2.0), 32.0); assert_eq!(resolve(SizeMetric::Rem, 2.0), 20.0); approx(resolve(SizeMetric::Vw, 10.0), 100.0); approx(resolve(SizeMetric::Vh, 10.0), 50.0); assert_eq!(resolve(SizeMetric::Vmin, 100.0), 500.0); assert_eq!(resolve(SizeMetric::Vmax, 100.0), 1000.0); assert_eq!(resolve(SizeMetric::In, 1.0), 96.0);
approx(resolve(SizeMetric::Cm, 2.54), 96.0);
approx(resolve(SizeMetric::Mm, 25.4), 96.0);
}
#[test]
fn resolve_size_metric_at_zero_and_negative_values() {
for m in [
SizeMetric::Px,
SizeMetric::Pt,
SizeMetric::Percent,
SizeMetric::Em,
SizeMetric::Rem,
SizeMetric::Vw,
SizeMetric::Vh,
SizeMetric::Vmin,
SizeMetric::Vmax,
SizeMetric::In,
SizeMetric::Cm,
SizeMetric::Mm,
] {
assert_eq!(resolve(m, 0.0), 0.0, "{m:?} at zero");
assert!(resolve(m, -10.0) <= 0.0, "{m:?} keeps the sign of a negative");
}
assert_eq!(
resolve_size_metric(SizeMetric::Percent, 50.0, -200.0, size(0.0, 0.0), 16.0, 16.0),
-100.0
);
}
#[test]
fn resolve_size_metric_saturates_instead_of_panicking_at_the_limits() {
let huge = resolve_size_metric(
SizeMetric::Em,
f32::MAX,
0.0,
size(0.0, 0.0),
f32::MAX,
16.0,
);
assert!(huge.is_infinite(), "MAX * MAX saturates to +inf");
let neg = resolve_size_metric(
SizeMetric::Percent,
f32::MIN,
f32::MAX,
size(0.0, 0.0),
16.0,
16.0,
);
assert!(neg.is_infinite() && neg.is_sign_negative());
}
#[test]
fn resolve_size_metric_propagates_nan_and_inf_without_panicking() {
assert!(resolve(SizeMetric::Px, f32::NAN).is_nan());
assert!(resolve(SizeMetric::Px, f32::INFINITY).is_infinite());
assert!(resolve(SizeMetric::Percent, f32::NAN).is_nan());
assert!(resolve_size_metric(
SizeMetric::Em,
0.0,
0.0,
size(0.0, 0.0),
f32::INFINITY,
16.0
)
.is_nan());
let vp = size(f32::NAN, 400.0);
assert_eq!(
resolve_size_metric(SizeMetric::Vmin, 100.0, 0.0, vp, 16.0, 16.0),
400.0
);
assert_eq!(
resolve_size_metric(SizeMetric::Vmax, 100.0, 0.0, vp, 16.0, 16.0),
400.0
);
}
#[test]
fn convert_font_style_maps_every_variant() {
assert_eq!(
convert_font_style(StyleFontStyle::Normal),
crate::font_traits::FontStyle::Normal
);
assert_eq!(
convert_font_style(StyleFontStyle::Italic),
crate::font_traits::FontStyle::Italic
);
assert_eq!(
convert_font_style(StyleFontStyle::Oblique),
crate::font_traits::FontStyle::Oblique
);
}
#[test]
fn convert_font_weight_maps_every_variant_monotonically() {
assert_eq!(convert_font_weight(StyleFontWeight::W100), FcWeight::Thin);
assert_eq!(
convert_font_weight(StyleFontWeight::W200),
FcWeight::ExtraLight
);
assert_eq!(convert_font_weight(StyleFontWeight::W300), FcWeight::Light);
assert_eq!(
convert_font_weight(StyleFontWeight::Lighter),
FcWeight::Light
);
assert_eq!(convert_font_weight(StyleFontWeight::Normal), FcWeight::Normal);
assert_eq!(convert_font_weight(StyleFontWeight::W500), FcWeight::Medium);
assert_eq!(
convert_font_weight(StyleFontWeight::W600),
FcWeight::SemiBold
);
assert_eq!(convert_font_weight(StyleFontWeight::Bold), FcWeight::Bold);
assert_eq!(
convert_font_weight(StyleFontWeight::W800),
FcWeight::ExtraBold
);
assert_eq!(convert_font_weight(StyleFontWeight::W900), FcWeight::Black);
assert_eq!(convert_font_weight(StyleFontWeight::Bolder), FcWeight::Black);
}
fn bi(width: f32, style: BorderStyle, source: BorderSource) -> BorderInfo {
BorderInfo::new(width, style, ColorU::BLACK, source)
}
#[test]
fn border_info_new_stores_its_arguments_verbatim() {
let b = BorderInfo::new(f32::NAN, BorderStyle::Dotted, ColorU::RED, BorderSource::Cell);
assert!(b.width.is_nan());
assert_eq!(b.style, BorderStyle::Dotted);
assert_eq!(b.color, ColorU::RED);
assert_eq!(b.source, BorderSource::Cell);
}
#[test]
fn border_style_priority_follows_the_css_ordering() {
assert_eq!(BorderInfo::style_priority(&BorderStyle::Hidden), 255);
assert_eq!(BorderInfo::style_priority(&BorderStyle::None), 0);
let ordered = [
BorderStyle::Double,
BorderStyle::Solid,
BorderStyle::Dashed,
BorderStyle::Dotted,
BorderStyle::Ridge,
BorderStyle::Outset,
BorderStyle::Groove,
BorderStyle::Inset,
];
for w in ordered.windows(2) {
assert!(
BorderInfo::style_priority(&w[0]) > BorderInfo::style_priority(&w[1]),
"{:?} must outrank {:?}",
w[0],
w[1]
);
}
}
#[test]
fn resolve_conflict_hidden_suppresses_everything() {
let hidden = bi(1.0, BorderStyle::Hidden, BorderSource::Table);
let fat = bi(99.0, BorderStyle::Solid, BorderSource::Cell);
assert!(BorderInfo::resolve_conflict(&hidden, &fat).is_none());
assert!(BorderInfo::resolve_conflict(&fat, &hidden).is_none());
}
#[test]
fn resolve_conflict_none_always_loses() {
let none = bi(50.0, BorderStyle::None, BorderSource::Cell);
let thin = bi(1.0, BorderStyle::Solid, BorderSource::Table);
assert!(BorderInfo::resolve_conflict(&none, &none).is_none());
assert_eq!(
BorderInfo::resolve_conflict(&none, &thin).unwrap().style,
BorderStyle::Solid
);
assert_eq!(
BorderInfo::resolve_conflict(&thin, &none).unwrap().style,
BorderStyle::Solid
);
}
#[test]
fn resolve_conflict_falls_through_width_then_style_then_source() {
let wide = bi(5.0, BorderStyle::Dotted, BorderSource::Table);
let narrow = bi(2.0, BorderStyle::Double, BorderSource::Cell);
assert_eq!(BorderInfo::resolve_conflict(&wide, &narrow).unwrap().width, 5.0);
let a = bi(3.0, BorderStyle::Dotted, BorderSource::Cell);
let b = bi(3.0, BorderStyle::Double, BorderSource::Table);
assert_eq!(
BorderInfo::resolve_conflict(&a, &b).unwrap().style,
BorderStyle::Double
);
let t = bi(3.0, BorderStyle::Solid, BorderSource::Table);
let c = bi(3.0, BorderStyle::Solid, BorderSource::Cell);
assert_eq!(
BorderInfo::resolve_conflict(&t, &c).unwrap().source,
BorderSource::Cell
);
assert_eq!(
BorderInfo::resolve_conflict(&c, &t).unwrap().source,
BorderSource::Cell
);
let first = BorderInfo::new(3.0, BorderStyle::Solid, ColorU::RED, BorderSource::Row);
let second = BorderInfo::new(3.0, BorderStyle::Solid, ColorU::GREEN, BorderSource::Row);
assert_eq!(
BorderInfo::resolve_conflict(&first, &second).unwrap().color,
ColorU::RED
);
}
#[test]
fn resolve_conflict_with_nan_widths_is_deterministic() {
let nan = bi(f32::NAN, BorderStyle::Solid, BorderSource::Table);
let ok = bi(1.0, BorderStyle::Solid, BorderSource::Cell);
let win = BorderInfo::resolve_conflict(&nan, &ok).unwrap();
assert_eq!(win.source, BorderSource::Cell); assert_eq!(win.width, 1.0);
let nan_b = bi(f32::NAN, BorderStyle::Solid, BorderSource::Table);
let other = bi(2.0, BorderStyle::Solid, BorderSource::Table);
assert!(BorderInfo::resolve_conflict(&nan_b, &other)
.unwrap()
.width
.is_nan());
}
#[test]
fn resolve_conflict_infinite_width_wins() {
let inf = bi(f32::INFINITY, BorderStyle::Inset, BorderSource::Table);
let solid = bi(f32::MAX, BorderStyle::Solid, BorderSource::Cell);
assert!(BorderInfo::resolve_conflict(&inf, &solid)
.unwrap()
.width
.is_infinite());
}
fn cols(n: usize, min: f32, max: f32) -> Vec<TableColumnInfo> {
(0..n)
.map(|_| TableColumnInfo {
min_width: min,
max_width: max,
computed_width: None,
})
.collect()
}
#[test]
fn distribute_cell_width_spreads_the_deficit_evenly() {
let mut c = cols(2, 10.0, 20.0);
let collapsed = std::collections::HashSet::new();
distribute_cell_width_across_columns(&mut c, 0, 2, 50.0, 30.0, &collapsed);
assert_eq!(c[0].min_width, 25.0);
assert_eq!(c[1].min_width, 25.0);
assert_eq!(c[0].max_width, 20.0);
assert_eq!(c[1].max_width, 20.0);
}
#[test]
fn distribute_cell_width_is_a_noop_when_the_span_overruns_the_columns() {
let mut c = cols(2, 10.0, 20.0);
let collapsed = std::collections::HashSet::new();
distribute_cell_width_across_columns(&mut c, 1, 5, 500.0, 500.0, &collapsed);
assert_eq!(c[0].min_width, 10.0);
assert_eq!(c[1].min_width, 10.0);
distribute_cell_width_across_columns(&mut c, 99, 1, 500.0, 500.0, &collapsed);
distribute_cell_width_across_columns(&mut c, usize::MAX, 0, 500.0, 500.0, &collapsed);
assert_eq!(c[0].min_width, 10.0);
}
#[test]
fn distribute_cell_width_with_zero_colspan_does_not_divide_by_zero() {
let mut c = cols(2, 10.0, 20.0);
let collapsed = std::collections::HashSet::new();
distribute_cell_width_across_columns(&mut c, 0, 0, 1000.0, 1000.0, &collapsed);
assert_eq!(c[0].min_width, 10.0);
assert_eq!(c[1].min_width, 10.0);
assert!(c[0].min_width.is_finite());
}
#[test]
fn distribute_cell_width_skips_fully_collapsed_spans() {
let mut c = cols(2, 10.0, 20.0);
let collapsed: std::collections::HashSet<usize> = [0, 1].into_iter().collect();
distribute_cell_width_across_columns(&mut c, 0, 2, 1000.0, 1000.0, &collapsed);
assert_eq!(c[0].min_width, 10.0);
assert_eq!(c[1].min_width, 10.0);
let collapsed_one: std::collections::HashSet<usize> = [0].into_iter().collect();
distribute_cell_width_across_columns(&mut c, 0, 2, 100.0, 0.0, &collapsed_one);
assert_eq!(c[0].min_width, 10.0, "collapsed column is untouched");
assert_eq!(c[1].min_width, 100.0, "10 + (100 - 10) / 1");
}
#[test]
fn distribute_cell_width_ignores_nan_and_saturates_on_inf() {
let mut c = cols(2, 10.0, 20.0);
let collapsed = std::collections::HashSet::new();
distribute_cell_width_across_columns(&mut c, 0, 2, f32::NAN, f32::NAN, &collapsed);
assert_eq!(c[0].min_width, 10.0);
assert_eq!(c[1].max_width, 20.0);
distribute_cell_width_across_columns(
&mut c,
0,
2,
f32::INFINITY,
f32::INFINITY,
&collapsed,
);
assert!(c[0].min_width.is_infinite());
assert!(c[1].max_width.is_infinite());
}
#[test]
fn distribute_cell_width_does_not_shrink_columns() {
let mut c = cols(2, 100.0, 200.0);
let collapsed = std::collections::HashSet::new();
distribute_cell_width_across_columns(&mut c, 0, 2, 1.0, 1.0, &collapsed);
assert_eq!(c[0].min_width, 100.0);
assert_eq!(c[0].max_width, 200.0);
distribute_cell_width_across_columns(&mut c, 0, 2, -1000.0, -1000.0, &collapsed);
assert_eq!(c[1].min_width, 100.0);
}
#[test]
fn is_cell_empty_treats_missing_and_childless_cells_as_empty() {
let tree = build_tree(
vec![hot(None, Some(size(10.0, 10.0)), &BoxProps::default())],
vec![LayoutNodeWarm::default()],
&[vec![]],
);
assert!(is_cell_empty(&tree, 0), "no children => empty");
assert!(is_cell_empty(&tree, 1), "out-of-range index => empty");
assert!(is_cell_empty(&tree, usize::MAX), "usize::MAX must not panic");
}
#[test]
fn is_cell_empty_uses_the_inline_layout_when_present() {
let bp = BoxProps::default();
let mut warm = vec![LayoutNodeWarm::default(), LayoutNodeWarm::default()];
warm[0].inline_layout_result = Some(empty_inline_layout());
let tree = build_tree(
vec![
hot(None, Some(size(10.0, 10.0)), &bp),
hot(Some(0), Some(size(10.0, 10.0)), &bp),
],
warm,
&[vec![1], vec![]],
);
assert!(is_cell_empty(&tree, 0));
let tree2 = build_tree(
vec![
hot(None, Some(size(10.0, 10.0)), &bp),
hot(Some(0), Some(size(10.0, 10.0)), &bp),
],
vec![LayoutNodeWarm::default(), LayoutNodeWarm::default()],
&[vec![1], vec![]],
);
assert!(!is_cell_empty(&tree2, 0));
}
#[test]
fn is_empty_block_requires_no_children_no_inline_content_and_no_height() {
let bp = BoxProps::default();
let empty_tree = build_tree(Vec::new(), Vec::new(), &[]);
assert!(is_empty_block(&empty_tree, 0));
assert!(is_empty_block(&empty_tree, usize::MAX));
let t = build_tree(
vec![hot(None, None, &bp)],
vec![LayoutNodeWarm::default()],
&[vec![]],
);
assert!(is_empty_block(&t, 0));
for h in [0.0, -5.0] {
let t = build_tree(
vec![hot(None, Some(size(100.0, h)), &bp)],
vec![LayoutNodeWarm::default()],
&[vec![]],
);
assert!(is_empty_block(&t, 0), "height {h} must count as empty");
}
let t = build_tree(
vec![hot(None, Some(size(100.0, 0.5)), &bp)],
vec![LayoutNodeWarm::default()],
&[vec![]],
);
assert!(!is_empty_block(&t, 0));
let t = build_tree(
vec![hot(None, None, &bp), hot(Some(0), None, &bp)],
vec![LayoutNodeWarm::default(), LayoutNodeWarm::default()],
&[vec![1], vec![]],
);
assert!(!is_empty_block(&t, 0));
let mut warm = vec![LayoutNodeWarm::default()];
warm[0].inline_layout_result = Some(empty_inline_layout());
let t = build_tree(vec![hot(None, None, &bp)], warm, &[vec![]]);
assert!(!is_empty_block(&t, 0));
}
#[test]
fn compute_cell_baseline_falls_back_to_the_content_edge() {
let bp = box_props(
EdgeSizes::default(),
edges(0.0, 0.0, 2.0, 0.0), edges(0.0, 0.0, 5.0, 0.0), );
let tree = build_tree(
vec![hot(None, Some(size(50.0, 100.0)), &bp)],
vec![LayoutNodeWarm::default()],
&[vec![]],
);
assert_eq!(compute_cell_baseline(0, &tree), 93.0);
assert_eq!(compute_cell_baseline(usize::MAX, &tree), 0.0);
}
#[test]
fn compute_cell_baseline_without_used_size_can_go_negative() {
let bp = box_props(
EdgeSizes::default(),
edges(0.0, 0.0, 2.0, 0.0),
edges(0.0, 0.0, 5.0, 0.0),
);
let tree = build_tree(
vec![hot(None, None, &bp)],
vec![LayoutNodeWarm::default()],
&[vec![]],
);
assert_eq!(compute_cell_baseline(0, &tree), -7.0);
}
#[test]
fn check_scrollbar_necessity_never_scrolls_for_visible_hidden_or_clip() {
for o in [
OverflowBehavior::Visible,
OverflowBehavior::Hidden,
OverflowBehavior::Clip,
] {
let r = check_scrollbar_necessity(size(9999.0, 9999.0), size(10.0, 10.0), o, o, 16.0);
assert!(!r.needs_horizontal, "{o:?}");
assert!(!r.needs_vertical, "{o:?}");
assert_eq!(r.scrollbar_width, 0.0);
assert_eq!(r.scrollbar_height, 0.0);
}
}
#[test]
fn check_scrollbar_necessity_always_scrolls_for_scroll_even_with_no_content() {
let r = check_scrollbar_necessity(
size(0.0, 0.0),
size(500.0, 500.0),
OverflowBehavior::Scroll,
OverflowBehavior::Scroll,
16.0,
);
assert!(r.needs_horizontal && r.needs_vertical);
assert_eq!(r.scrollbar_width, 16.0);
assert_eq!(r.scrollbar_height, 16.0);
}
#[test]
fn check_scrollbar_necessity_auto_honours_the_one_pixel_epsilon() {
let auto = OverflowBehavior::Auto;
let r = check_scrollbar_necessity(size(301.0, 301.0), size(300.0, 300.0), auto, auto, 0.0);
assert!(!r.needs_horizontal);
assert!(!r.needs_vertical);
let r = check_scrollbar_necessity(size(302.0, 302.0), size(300.0, 300.0), auto, auto, 0.0);
assert!(r.needs_horizontal && r.needs_vertical);
assert_eq!(r.scrollbar_width, 0.0);
assert_eq!(r.scrollbar_height, 0.0);
}
#[test]
fn check_scrollbar_necessity_two_pass_adds_the_second_scrollbar() {
let auto = OverflowBehavior::Auto;
let r = check_scrollbar_necessity(size(300.0, 400.0), size(300.0, 300.0), auto, auto, 16.0);
assert!(r.needs_vertical);
assert!(r.needs_horizontal, "vertical scrollbar must force a horizontal one");
assert_eq!(r.scrollbar_width, 16.0);
assert_eq!(r.scrollbar_height, 16.0);
let r = check_scrollbar_necessity(size(300.0, 400.0), size(300.0, 300.0), auto, auto, 0.0);
assert!(r.needs_vertical);
assert!(!r.needs_horizontal);
}
#[test]
fn check_scrollbar_necessity_with_nan_and_negative_sizes_is_deterministic() {
let auto = OverflowBehavior::Auto;
let r = check_scrollbar_necessity(
size(f32::NAN, f32::NAN),
size(300.0, 300.0),
auto,
auto,
16.0,
);
assert!(!r.needs_horizontal && !r.needs_vertical);
let r = check_scrollbar_necessity(
size(500.0, 500.0),
size(f32::NAN, f32::NAN),
auto,
auto,
16.0,
);
assert!(!r.needs_horizontal && !r.needs_vertical);
let r = check_scrollbar_necessity(
size(0.0, 0.0),
size(-100.0, -100.0),
auto,
auto,
16.0,
);
assert!(r.needs_horizontal && r.needs_vertical);
let r = check_scrollbar_necessity(
size(f32::INFINITY, f32::INFINITY),
size(300.0, 300.0),
auto,
auto,
16.0,
);
assert!(r.needs_horizontal && r.needs_vertical);
}
#[test]
fn check_scrollbar_necessity_with_negative_scrollbar_width_skips_the_second_pass() {
let auto = OverflowBehavior::Auto;
let r =
check_scrollbar_necessity(size(300.0, 400.0), size(300.0, 300.0), auto, auto, -16.0);
assert!(r.needs_vertical);
assert!(!r.needs_horizontal, "the `> 0.0` guard skips the two-pass check");
assert_eq!(r.scrollbar_width, -16.0); }
#[test]
fn collapse_margins_follows_css_2_1_section_8_3_1() {
assert_eq!(collapse_margins(10.0, 20.0), 20.0); assert_eq!(collapse_margins(-10.0, -20.0), -20.0); assert_eq!(collapse_margins(20.0, -5.0), 15.0); assert_eq!(collapse_margins(-5.0, 20.0), 15.0);
assert_eq!(collapse_margins(0.0, 0.0), 0.0);
assert_eq!(collapse_margins(0.0, 10.0), 10.0);
assert_eq!(collapse_margins(0.0, -5.0), -5.0);
}
#[test]
fn collapse_margins_is_commutative_for_finite_inputs() {
let vals = [-100.0_f32, -1.0, -0.5, 0.0, 0.5, 1.0, 100.0, f32::MAX, f32::MIN];
for a in vals {
for b in vals {
let ab = collapse_margins(a, b);
let ba = collapse_margins(b, a);
assert_eq!(ab.to_bits(), ba.to_bits(), "collapse_margins({a}, {b})");
}
}
}
#[test]
fn collapse_margins_saturates_and_defines_nan_inf_behaviour() {
assert!(collapse_margins(f32::MAX, -f32::MAX).abs() < 1.0); assert_eq!(collapse_margins(f32::INFINITY, 5.0), f32::INFINITY);
assert_eq!(collapse_margins(f32::NEG_INFINITY, -5.0), f32::NEG_INFINITY);
assert!(collapse_margins(f32::INFINITY, f32::NEG_INFINITY).is_nan());
assert_eq!(collapse_margins(f32::NAN, 1.0), 1.0);
assert_eq!(collapse_margins(1.0, f32::NAN), 1.0);
assert_eq!(collapse_margins(-f32::NAN, -1.0), -1.0);
}
#[test]
fn advance_pen_with_margin_collapse_advances_by_exactly_the_collapsed_margin() {
let mut pen = 100.0_f32;
let collapsed = advance_pen_with_margin_collapse(&mut pen, 10.0, 20.0);
assert_eq!(collapsed, 20.0);
assert_eq!(pen, 120.0);
let mut pen = 100.0_f32;
let collapsed = advance_pen_with_margin_collapse(&mut pen, 30.0, -10.0);
assert_eq!(collapsed, 20.0);
assert_eq!(pen, 120.0);
let mut pen = 7.5_f32;
assert_eq!(advance_pen_with_margin_collapse(&mut pen, 0.0, 0.0), 0.0);
assert_eq!(pen, 7.5);
}
#[test]
fn advance_pen_with_margin_collapse_saturates_at_the_f32_limits() {
let mut pen = f32::MAX;
let collapsed = advance_pen_with_margin_collapse(&mut pen, f32::MAX, f32::MAX);
assert_eq!(collapsed, f32::MAX);
assert!(pen.is_infinite(), "MAX + MAX saturates to +inf, no panic");
let mut pen = f32::NAN;
let collapsed = advance_pen_with_margin_collapse(&mut pen, 1.0, 2.0);
assert_eq!(collapsed, 2.0);
assert!(pen.is_nan(), "a NaN pen stays NaN");
}
#[test]
fn has_margin_collapse_blocker_basic_true_false() {
let none = BoxProps::default();
assert!(!has_margin_collapse_blocker(&none, HTB, true));
assert!(!has_margin_collapse_blocker(&none, HTB, false));
let top_border = box_props(
EdgeSizes::default(),
edges(1.0, 0.0, 0.0, 0.0),
EdgeSizes::default(),
);
assert!(has_margin_collapse_blocker(&top_border, HTB, true));
assert!(!has_margin_collapse_blocker(&top_border, HTB, false));
let bottom_padding = box_props(
EdgeSizes::default(),
EdgeSizes::default(),
edges(0.0, 0.0, 0.5, 0.0),
);
assert!(!has_margin_collapse_blocker(&bottom_padding, HTB, true));
assert!(has_margin_collapse_blocker(&bottom_padding, HTB, false));
}
#[test]
fn has_margin_collapse_blocker_maps_edges_per_writing_mode() {
let left_border = box_props(
EdgeSizes::default(),
edges(0.0, 0.0, 0.0, 3.0),
EdgeSizes::default(),
);
assert!(has_margin_collapse_blocker(&left_border, VRL, true));
assert!(!has_margin_collapse_blocker(&left_border, VRL, false));
assert!(!has_margin_collapse_blocker(&left_border, HTB, true));
assert!(!has_margin_collapse_blocker(&left_border, HTB, false));
}
#[test]
fn has_margin_collapse_blocker_ignores_negative_and_nan_edges() {
let weird = box_props(
EdgeSizes::default(),
edges(-5.0, 0.0, f32::NAN, 0.0),
edges(f32::NAN, 0.0, -1.0, 0.0),
);
assert!(!has_margin_collapse_blocker(&weird, HTB, true));
assert!(!has_margin_collapse_blocker(&weird, HTB, false));
let inf = box_props(
EdgeSizes::default(),
edges(f32::INFINITY, 0.0, 0.0, 0.0),
EdgeSizes::default(),
);
assert!(has_margin_collapse_blocker(&inf, HTB, true));
}
#[test]
fn is_bk_or_nl_class_covers_exactly_vt_ff_nel_ls_ps() {
for c in ['\u{000B}', '\u{000C}', '\u{0085}', '\u{2028}', '\u{2029}'] {
assert!(is_bk_or_nl_class(c), "{:04X} must be BK/NL", c as u32);
}
for c in ['\n', '\r', ' ', '\t', 'a', '\0', '\u{200B}', char::MAX] {
assert!(!is_bk_or_nl_class(c), "{:04X} must not be BK/NL", c as u32);
}
}
#[test]
fn is_bidi_control_covers_the_uax9_set_only() {
for c in [
'\u{200E}', '\u{200F}', '\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}',
'\u{2066}', '\u{2067}', '\u{2068}', '\u{2069}', '\u{061C}',
] {
assert!(is_bidi_control(c), "{:04X} must be a bidi control", c as u32);
}
for c in ['a', ' ', '\u{200B}', '\u{2028}', '\u{202F}', '\u{2065}', char::MAX] {
assert!(!is_bidi_control(c), "{:04X} must not be a bidi control", c as u32);
}
}
#[test]
fn is_css_document_whitespace_excludes_other_unicode_spaces() {
for c in [' ', '\t', '\n', '\r', '\x0C'] {
assert!(is_css_document_whitespace(c));
}
for c in ['\u{00A0}', '\u{3000}', '\u{200B}', '\u{2028}', '\u{000B}', 'a'] {
assert!(
!is_css_document_whitespace(c),
"{:04X} must not be document white space",
c as u32
);
}
}
#[test]
fn split_at_forced_breaks_handles_every_newline_flavour() {
assert_eq!(split_at_forced_breaks(""), vec![String::new()]);
assert_eq!(split_at_forced_breaks("abc"), vec!["abc".to_string()]);
assert_eq!(split_at_forced_breaks("a\nb"), vec!["a", "b"]);
assert_eq!(split_at_forced_breaks("a\r\nb"), vec!["a", "b"]);
assert_eq!(split_at_forced_breaks("a\rb"), vec!["a", "b"]);
assert_eq!(split_at_forced_breaks("a\r\rb"), vec!["a", "", "b"]);
assert_eq!(split_at_forced_breaks("\na\n"), vec!["", "a", ""]);
assert_eq!(split_at_forced_breaks("a\u{2028}b"), vec!["a", "b"]);
assert_eq!(split_at_forced_breaks("a\u{000B}b"), vec!["a", "b"]);
}
#[test]
fn split_at_forced_breaks_preserves_every_non_break_char() {
let text = "héllo 🌍\u{0301}\u{200B}x";
let segs = split_at_forced_breaks(text);
assert_eq!(segs.len(), 1);
assert_eq!(segs[0], text, "no char-boundary slicing damage");
let many = "a\nb\rc\r\nd\u{2029}e";
assert_eq!(split_at_forced_breaks(many).len(), 5);
assert_eq!(split_at_forced_breaks(many).concat(), "abcde");
}
#[test]
fn split_at_forced_breaks_survives_a_huge_all_break_input() {
let text = "\n".repeat(10_000);
let segs = split_at_forced_breaks(&text);
assert_eq!(segs.len(), 10_001);
assert!(segs.iter().all(String::is_empty));
}
#[test]
fn split_at_bk_nl_chars_ignores_lf_and_cr() {
assert_eq!(split_at_bk_nl_chars(""), vec![String::new()]);
assert_eq!(split_at_bk_nl_chars("a\nb\rc"), vec!["a\nb\rc".to_string()]);
assert_eq!(split_at_bk_nl_chars("a\u{0085}b"), vec!["a", "b"]);
assert_eq!(split_at_bk_nl_chars("\u{2029}"), vec!["", ""]);
}
#[test]
fn is_east_asian_wide_matches_cjk_kana_and_hangul() {
for c in ['中', '一', 'あ', 'カ', '。', '가', 'ㄅ', 'A'] {
assert!(is_east_asian_wide(c), "{:04X} must be wide", c as u32);
}
for c in ['a', 'Z', '0', ' ', 'é', '\u{0000}', char::MAX] {
assert!(!is_east_asian_wide(c), "{:04X} must not be wide", c as u32);
}
}
#[test]
fn is_east_asian_wide_range_boundaries_are_inclusive() {
assert!(!is_east_asian_wide('\u{4DFF}')); assert!(is_east_asian_wide('\u{4E00}')); assert!(is_east_asian_wide('\u{9FFF}')); assert!(!is_east_asian_wide('\u{A000}')); assert!(!is_east_asian_wide('\u{FF00}')); assert!(is_east_asian_wide('\u{FF01}')); assert!(is_east_asian_wide('\u{FF60}')); assert!(!is_east_asian_wide('\u{FF61}')); }
#[test]
fn is_east_asian_fullwidth_or_wide_excludes_hangul_but_wide_does_not() {
assert!(is_east_asian_wide('가'));
assert!(!is_east_asian_fullwidth_or_wide('가'));
assert!(!is_east_asian_fullwidth_or_wide('\u{1100}')); assert!(!is_east_asian_fullwidth_or_wide('\u{3130}')); assert!(!is_east_asian_fullwidth_or_wide('\u{A960}')); assert!(!is_east_asian_fullwidth_or_wide('\u{D7B0}'));
assert!(is_east_asian_fullwidth_or_wide('中'));
assert!(is_east_asian_fullwidth_or_wide('あ'));
assert!(is_east_asian_fullwidth_or_wide('\u{FF61}'));
assert!(is_east_asian_fullwidth_or_wide('\u{A000}'));
assert!(!is_east_asian_fullwidth_or_wide('a'));
assert!(!is_east_asian_fullwidth_or_wide(char::MAX));
}
#[test]
fn apply_segment_break_transform_converts_breaks_to_a_single_space() {
assert_eq!(apply_segment_break_transform(""), "");
assert_eq!(apply_segment_break_transform("ab"), "ab");
assert_eq!(apply_segment_break_transform("a\nb"), "a b");
assert_eq!(apply_segment_break_transform("a\r\nb"), "a b");
assert_eq!(apply_segment_break_transform("a\rb"), "a b");
assert_eq!(apply_segment_break_transform("a \n b"), "a b");
assert_eq!(apply_segment_break_transform("a\t\n\tb"), "a b");
assert_eq!(apply_segment_break_transform("a\n\n\nb"), "a b");
}
#[test]
fn apply_segment_break_transform_at_the_string_edges() {
assert_eq!(apply_segment_break_transform("\na"), " a");
assert_eq!(apply_segment_break_transform("a\n"), "a ");
assert_eq!(apply_segment_break_transform("\n"), " ");
assert_eq!(apply_segment_break_transform(" \n "), " ");
}
#[test]
fn apply_segment_break_transform_removes_breaks_around_zwsp_and_cjk() {
assert_eq!(apply_segment_break_transform("a\u{200B}\nb"), "a\u{200B}b");
assert_eq!(apply_segment_break_transform("a\n\u{200B}b"), "a\u{200B}b");
assert_eq!(apply_segment_break_transform("中\n文"), "中文");
assert_eq!(apply_segment_break_transform("あ \n い"), "あい");
assert_eq!(apply_segment_break_transform("中\na"), "中 a");
assert_eq!(apply_segment_break_transform("a\n中"), "a 中");
assert_eq!(apply_segment_break_transform("가\n나"), "가 나");
}
#[test]
fn apply_segment_break_transform_survives_pathological_input() {
let text = "\n".repeat(5_000);
assert_eq!(apply_segment_break_transform(&text), " ");
let mixed = "🌍\n🌍\n🌍";
assert_eq!(apply_segment_break_transform(mixed), "🌍 🌍 🌍");
}
#[test]
fn apply_text_transform_none_is_the_identity() {
for s in ["", "abc", "ÄÖÜ", "🌍", " \t\n"] {
assert_eq!(apply_text_transform(s, TextTransform::None), s);
}
}
#[test]
fn apply_text_transform_case_changes_handle_growing_and_multibyte_chars() {
assert_eq!(apply_text_transform("abc", TextTransform::Uppercase), "ABC");
assert_eq!(apply_text_transform("straße", TextTransform::Uppercase), "STRASSE");
assert_eq!(apply_text_transform("ÄÖÜ", TextTransform::Lowercase), "äöü");
assert_eq!(apply_text_transform("", TextTransform::Uppercase), "");
assert_eq!(apply_text_transform("🌍", TextTransform::Uppercase), "🌍");
}
#[test]
fn apply_text_transform_capitalize_uses_word_boundaries() {
assert_eq!(
apply_text_transform("hello world", TextTransform::Capitalize),
"Hello World"
);
assert_eq!(
apply_text_transform("1st place", TextTransform::Capitalize),
"1st Place"
);
assert_eq!(
apply_text_transform("a-b (c)", TextTransform::Capitalize),
"A-B (C)"
);
assert_eq!(apply_text_transform("élan", TextTransform::Capitalize), "Élan");
assert_eq!(apply_text_transform("", TextTransform::Capitalize), "");
}
#[test]
fn apply_text_transform_fullwidth_maps_the_ascii_block() {
assert_eq!(apply_text_transform("a", TextTransform::FullWidth), "\u{FF41}");
assert_eq!(apply_text_transform("!", TextTransform::FullWidth), "\u{FF01}");
assert_eq!(apply_text_transform("~", TextTransform::FullWidth), "\u{FF5E}");
assert_eq!(apply_text_transform(" ", TextTransform::FullWidth), "\u{3000}");
assert_eq!(apply_text_transform("\t\n", TextTransform::FullWidth), "\t\n");
assert_eq!(apply_text_transform("\u{7F}", TextTransform::FullWidth), "\u{7F}");
assert_eq!(apply_text_transform("中🌍", TextTransform::FullWidth), "中🌍");
}
#[test]
fn layout_initial_letter_rejects_degenerate_parameters() {
assert_eq!(layout_initial_letter(0.0, 3, 1000.0, 20.0), (0.0, 0.0));
assert_eq!(layout_initial_letter(-1.0, 3, 1000.0, 20.0), (0.0, 0.0));
assert_eq!(layout_initial_letter(3.0, 3, 1000.0, 0.0), (0.0, 0.0));
assert_eq!(layout_initial_letter(3.0, 3, 1000.0, -20.0), (0.0, 0.0));
assert_eq!(layout_initial_letter(3.0, 3, 0.0, 20.0), (0.0, 0.0));
assert_eq!(layout_initial_letter(3.0, 3, -10.0, 20.0), (0.0, 0.0));
}
#[test]
fn layout_initial_letter_computes_a_classic_drop_cap() {
assert_eq!(layout_initial_letter(3.0, 3, 1000.0, 20.0), (46.0, 60.0));
assert_eq!(layout_initial_letter(3.0, 3, 10.0, 20.0), (10.0, 60.0));
assert_eq!(layout_initial_letter(3.0, 0, 1000.0, 20.0), (46.0, 60.0));
assert_eq!(layout_initial_letter(2.0, 5, 1000.0, 20.0), (32.0, 100.0));
}
#[test]
fn layout_initial_letter_exclusion_is_never_shorter_than_the_letter() {
for size_lines in [0.5_f32, 1.0, 3.0, 100.0] {
for sink in [0_u32, 1, 3, 100] {
let (w, h) = layout_initial_letter(size_lines, sink, 10_000.0, 20.0);
let letter_height = size_lines * 20.0;
assert!(h >= letter_height, "{size_lines}/{sink}: {h} < {letter_height}");
assert!(w > 0.0 && w.is_finite());
}
}
}
#[test]
fn layout_initial_letter_saturates_at_extreme_sinks_and_sizes() {
let (w, h) = layout_initial_letter(1.0, u32::MAX, 1000.0, 20.0);
assert!(h.is_finite() && h > 0.0);
assert_eq!(w, 18.0);
let (w, h) = layout_initial_letter(f32::MAX, 1, 1000.0, f32::MAX);
assert_eq!(w, 1000.0);
assert!(h.is_infinite());
}
#[test]
fn layout_initial_letter_with_nan_and_inf_produces_a_defined_result() {
let (w, h) = layout_initial_letter(f32::NAN, 3, 1000.0, 20.0);
assert_eq!(w, 1000.0);
assert_eq!(h, 60.0);
let (w, h) = layout_initial_letter(3.0, 3, 1000.0, f32::INFINITY);
assert_eq!(w, 1000.0);
assert!(h.is_infinite());
}
fn cell_dom(attrs: Vec<AttributeType>) -> StyledDom {
let mut cell = Dom::create_div();
for a in attrs {
cell = cell.with_attribute(a);
}
styled(Dom::create_body().with_child(cell), "")
}
#[test]
fn get_cell_spans_defaults_to_one_when_unset() {
let dom = cell_dom(Vec::new());
assert_eq!(get_cell_spans(&dom, DIV_NODE), (1, 1));
}
#[test]
fn get_cell_spans_clamps_hostile_html_values() {
for n in [0, -1, i32::MIN] {
let dom = cell_dom(vec![AttributeType::ColSpan(n), AttributeType::RowSpan(n)]);
assert_eq!(get_cell_spans(&dom, DIV_NODE), (1, 1), "span {n}");
}
let dom = cell_dom(vec![
AttributeType::ColSpan(i32::MAX),
AttributeType::RowSpan(i32::MAX),
]);
assert_eq!(get_cell_spans(&dom, DIV_NODE), (1000, 65534));
let dom = cell_dom(vec![
AttributeType::ColSpan(3),
AttributeType::RowSpan(7),
]);
assert_eq!(get_cell_spans(&dom, DIV_NODE), (3, 7));
}
#[test]
fn get_float_and_clear_properties_default_to_none_for_a_missing_node() {
let dom = text_dom("x", "");
assert_eq!(get_float_property(&dom, None), LayoutFloat::None);
assert_eq!(get_clear_property(&dom, None), LayoutClear::None);
assert_eq!(get_float_property(&dom, Some(DIV_NODE)), LayoutFloat::None);
assert_eq!(get_clear_property(&dom, Some(DIV_NODE)), LayoutClear::None);
}
#[test]
fn get_float_and_clear_properties_read_the_cascade() {
let dom = text_dom("x", ".p { float: right; clear: both; }");
assert_eq!(get_float_property(&dom, Some(DIV_NODE)), LayoutFloat::Right);
assert_eq!(get_clear_property(&dom, Some(DIV_NODE)), LayoutClear::Both);
let dom = text_dom("x", ".p { float: left; clear: left; }");
assert_eq!(get_float_property(&dom, Some(DIV_NODE)), LayoutFloat::Left);
assert_eq!(get_clear_property(&dom, Some(DIV_NODE)), LayoutClear::Left);
}
#[test]
fn split_text_for_whitespace_collapses_runs_in_normal_mode() {
let dom = text_dom(" a b ", "");
let out = split_text_for_whitespace(&dom, TEXT_NODE, " a b ", &plain_style());
assert_eq!(out.len(), 1);
assert_eq!(text_of(&out[0]), Some(" a b "));
}
#[test]
fn split_text_for_whitespace_on_empty_and_whitespace_only_text() {
let dom = text_dom("", "");
assert!(split_text_for_whitespace(&dom, TEXT_NODE, "", &plain_style()).is_empty());
let out = split_text_for_whitespace(&dom, TEXT_NODE, " \t ", &plain_style());
assert_eq!(out.len(), 1);
assert_eq!(text_of(&out[0]), Some(" "));
}
#[test]
fn split_text_for_whitespace_honours_newlines_in_pre() {
let dom = text_dom("a\nb", ".p { white-space: pre; }");
let out = split_text_for_whitespace(&dom, TEXT_NODE, "a\nb", &plain_style());
assert_eq!(out.len(), 3);
assert_eq!(text_of(&out[0]), Some("a"));
assert!(matches!(out[1], InlineContent::LineBreak(_)));
assert_eq!(text_of(&out[2]), Some("b"));
let out = split_text_for_whitespace(&dom, TEXT_NODE, "a\tb", &plain_style());
assert_eq!(out.len(), 3);
assert_eq!(text_of(&out[0]), Some("a"));
assert!(matches!(out[1], InlineContent::Tab { .. }));
assert_eq!(text_of(&out[2]), Some("b"));
let out = split_text_for_whitespace(&dom, TEXT_NODE, " a ", &plain_style());
assert_eq!(text_of(&out[0]), Some(" a "));
}
#[test]
fn split_text_for_whitespace_normalises_cr_and_crlf() {
let dom = text_dom("x", ".p { white-space: pre; }");
for text in ["a\r\nb", "a\rb", "a\nb"] {
let out = split_text_for_whitespace(&dom, TEXT_NODE, text, &plain_style());
assert_eq!(out.len(), 3, "{text:?}");
assert!(matches!(out[1], InlineContent::LineBreak(_)), "{text:?}");
}
}
#[test]
fn split_text_for_whitespace_forces_breaks_on_bk_nl_chars_in_every_mode() {
let dom = text_dom("x", "");
let out = split_text_for_whitespace(&dom, TEXT_NODE, "a\u{2028}b", &plain_style());
assert_eq!(out.len(), 3);
assert!(matches!(out[1], InlineContent::LineBreak(_)));
let dom = text_dom("x", ".p { white-space: pre-line; }");
let out = split_text_for_whitespace(&dom, TEXT_NODE, "a\u{000C}b", &plain_style());
assert_eq!(out.len(), 3);
assert!(matches!(out[1], InlineContent::LineBreak(_)));
}
#[test]
fn split_text_for_whitespace_strips_bidi_controls_before_collapsing() {
let dom = text_dom("x", "");
let out = split_text_for_whitespace(&dom, TEXT_NODE, "a \u{200F} b", &plain_style());
assert_eq!(out.len(), 1);
assert_eq!(text_of(&out[0]), Some("a b"));
}
#[test]
fn split_text_for_whitespace_pre_line_collapses_spaces_but_keeps_breaks() {
let dom = text_dom("x", ".p { white-space: pre-line; }");
let out = split_text_for_whitespace(&dom, TEXT_NODE, "a b\n c ", &plain_style());
assert_eq!(out.len(), 3);
assert_eq!(text_of(&out[0]), Some("a b"));
assert!(matches!(out[1], InlineContent::LineBreak(_)));
assert_eq!(text_of(&out[2]), Some("c"));
}
#[test]
fn split_text_for_whitespace_survives_a_huge_multibyte_input() {
let dom = text_dom("x", ".p { white-space: pre; }");
let text = "🌍\n".repeat(2_000);
let out = split_text_for_whitespace(&dom, TEXT_NODE, &text, &plain_style());
assert_eq!(out.len(), 4_000);
assert_eq!(text_of(&out[0]), Some("🌍"));
}
}