use super::*;
use crate::layout::elements::{Container, Image, LayoutNode, LayoutVisitor, TextBlock};
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct FlowPosition {
pub(super) y: f32,
pub(super) cursor_y: f32,
pub(super) previous_margin_bottom: f32,
}
impl FlowPosition {
pub(super) const fn new(y: f32, cursor_y: f32, previous_margin_bottom: f32) -> Self {
Self {
y,
cursor_y,
previous_margin_bottom,
}
}
}
pub(super) fn collapsed_margin_top_extra(margin_top: f32, prev_margin_bottom: f32) -> f32 {
let collapsed = if margin_top >= 0.0 && prev_margin_bottom >= 0.0 {
margin_top.max(prev_margin_bottom)
} else if margin_top < 0.0 && prev_margin_bottom < 0.0 {
margin_top.min(prev_margin_bottom)
} else {
margin_top + prev_margin_bottom
};
collapsed - prev_margin_bottom
}
pub(super) fn clear_cursor(
cursor_y: f32,
clear: Clear,
left_bottom: f32,
right_bottom: f32,
prev_margin_bottom: &mut f32,
) -> f32 {
let clear_to = match clear {
Clear::Left => left_bottom,
Clear::Right => right_bottom,
Clear::Both => left_bottom.min(right_bottom),
Clear::None => return cursor_y,
};
if clear_to < cursor_y {
*prev_margin_bottom = 0.0;
clear_to
} else {
cursor_y
}
}
pub(super) enum CollapseRole {
Collapsing(f32, f32),
Skip,
Barrier,
}
pub(super) fn collapse_role(element: &dyn LayoutElement) -> CollapseRole {
if element.fragment_placement_owner().is_some() {
return CollapseRole::Skip;
}
if element
.positioning_owner()
.is_some_and(|owner| owner.positioning().scheme.is_absolute())
{
return CollapseRole::Skip;
}
let Some(participant) = element.block_flow_participant() else {
return CollapseRole::Barrier;
};
if !participant.is_in_flow_block() || !participant.collapses_outer_margins() {
return CollapseRole::Barrier;
}
let margins = participant.margins();
CollapseRole::Collapsing(margins.start, margins.end)
}
pub(super) fn child_explicit_width(element: &dyn LayoutElement) -> Option<f32> {
#[derive(Default)]
struct Width(Option<f32>);
impl LayoutVisitor for Width {
fn visit_container(&mut self, element: &Container) {
self.0 = element.box_model.size.width.fixed_value();
}
fn visit_text_block(&mut self, element: &TextBlock) {
self.0 = element.box_model.size.width.fixed_value();
}
fn visit_image(&mut self, element: &Image) {
self.0 = Some(element.geometry.size.width);
}
}
let mut width = Width::default();
element.accept(&mut width);
width.0
}
pub(super) fn children_overflow_extent(children: &[LayoutNode]) -> (f32, f32) {
let w = children
.iter()
.filter_map(|child| child_explicit_width(child.as_ref()))
.fold(0.0f32, f32::max);
(w, collapsed_children_height(children))
}
pub(super) fn collapsed_children_height(children: &[LayoutNode]) -> f32 {
if children
.iter()
.any(|c| crate::layout::paginate::element_float(c) != Float::None)
{
return crate::layout::paginate::simulate_block_flow(children).height;
}
let mut total = 0.0f32;
let mut prev_mb: Option<f32> = None;
for child in children {
total += crate::layout::engine::estimate_element_height(child);
match collapse_role(child) {
CollapseRole::Collapsing(mt, mb) => {
if let Some(pmb) = prev_mb {
let collapsed = if mt >= 0.0 && pmb >= 0.0 {
mt.max(pmb)
} else if mt < 0.0 && pmb < 0.0 {
mt.min(pmb)
} else {
mt + pmb
};
total -= pmb + mt - collapsed;
}
prev_mb = Some(mb);
}
CollapseRole::Skip => {}
CollapseRole::Barrier => prev_mb = None,
}
}
total
}
pub(super) fn child_paint_order(
element: &dyn LayoutElement,
) -> crate::layout::elements::StackingLevel {
crate::layout::engine::layout_element_paint_order(element)
}
pub(super) fn abs_child_anchor(
cb: &Option<crate::layout::engine::ContainingBlock>,
abs_origins: &HashMap<usize, PdfPoint>,
self_pad_origin: PdfPoint,
) -> PdfPoint {
cb.and_then(|c| abs_origins.get(&c.depth).copied())
.unwrap_or(self_pad_origin)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::elements::{IntoLayoutNode, TextBlock};
use crate::layout::flow_metrics::{BlockFlowSpacing, BlockMargins};
fn empty_block(margins: BlockMargins) -> LayoutNode {
TextBlock {
box_model: crate::layout::elements::BoxModel {
margins,
..Default::default()
},
..Default::default()
}
.boxed()
}
#[test]
fn table_internal_spacing_stays_outside_sibling_margin_collapse() {
let table = TableRow {
flow: BlockFlowSpacing {
margins: BlockMargins::new(4.0, 6.0),
internal: BlockMargins::new(2.0, 3.0),
..Default::default()
},
..Default::default()
}
.boxed();
let children = vec![
empty_block(BlockMargins::new(0.0, 10.0)),
table,
empty_block(BlockMargins::new(8.0, 0.0)),
];
assert_eq!(
crate::layout::paginate::simulate_block_flow(&children).height,
23.0
);
assert_eq!(collapsed_children_height(&children), 23.0);
}
}