use std::borrow::Cow;
use std::collections::HashMap;
use std::rc::Rc;
use crate::fonts::FontCollection;
use crate::html::NodeId;
use crate::pdf::PreparedImage;
use crate::style::{
BorderCollapse, BorderStyle, BoxSizing, BreakBetween, BreakInside, CaptionSide, Clear,
ComputedStyle, Display, Float, Length, LengthPercentage, LengthPercentageOrAuto, MaxSize,
Position,
};
use super::box_tree::{BoxContent, ImageBoxContent, LayoutBox, RelativeInset, TableSection};
use super::flex::layout_flex;
use super::float_ctx::FloatContext;
use super::geometry::{EdgeSizes, FragmentPosition, Layout, Rect};
use super::grid::{layout_grid, LaidOutGrid};
use super::inline::{
apply_text_overflow, clear_word_width_cache, finish_line, layout_inline_content, shape_run,
LineBox,
};
use super::table::layout_table;
const LIST_MARKER_GAP: f32 = 8.0;
#[derive(Debug, Clone)]
pub struct LaidOutBox {
pub node: Option<NodeId>,
pub layout: Layout,
pub fragmentation: FragmentationHints,
pub has_visible_decoration: bool,
pub is_float: bool,
pub content: LaidOutContent,
pub marker: Option<Box<LineBox>>,
pub link: Option<Rc<str>>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FragmentationHints {
pub break_before: BreakBetween,
pub break_after: BreakBetween,
pub break_inside: BreakInside,
pub orphans: u32,
pub widows: u32,
}
impl From<&ComputedStyle> for FragmentationHints {
fn from(style: &ComputedStyle) -> Self {
Self {
break_before: style.break_before,
break_after: style.break_after,
break_inside: style.break_inside,
orphans: style.orphans,
widows: style.widows,
}
}
}
impl Default for FragmentationHints {
fn default() -> Self {
Self::from(&ComputedStyle::default())
}
}
#[derive(Debug, Clone)]
pub enum LaidOutContent {
Blocks(Vec<LaidOutBox>),
Inline(Vec<LineBox>),
Table(LaidOutTable),
Flex(Vec<LaidOutBox>),
Grid(LaidOutGrid),
Image(Option<Rc<PreparedImage>>),
}
#[derive(Debug, Clone)]
pub struct LaidOutTable {
pub caption: Option<Box<LaidOutBox>>,
pub caption_side: CaptionSide,
pub rows: Vec<LaidOutTableRow>,
}
#[derive(Debug, Clone)]
pub struct LaidOutTableRow {
pub node: Option<NodeId>,
pub cells: Vec<LaidOutBox>,
pub section: TableSection,
pub group: Option<NodeId>,
}
#[derive(Debug, Clone)]
pub struct PositionedBox {
pub laid: LaidOutBox,
pub kind: PositionedKind,
}
#[derive(Debug, Clone, Copy)]
pub enum PositionedKind {
Fixed,
AbsoluteInitial,
AbsoluteAncestor {
node: NodeId,
padding_box_origin: (f32, f32),
},
}
pub(super) struct PosCtx<'a> {
abs_cb: AbsCB,
page_size: (f32, f32),
cb_height: Option<f32>,
out: &'a mut Vec<PositionedBox>,
}
#[derive(Debug, Clone, Copy)]
enum AbsCB {
InitialPage,
Ancestor {
node: NodeId,
rect: Rect,
height: Option<f32>,
},
}
impl<'a> PosCtx<'a> {
pub(super) fn cb_height(&self) -> Option<f32> {
self.cb_height
}
pub(super) fn new(out: &'a mut Vec<PositionedBox>, page_size: (f32, f32)) -> Self {
Self {
abs_cb: AbsCB::InitialPage,
page_size,
cb_height: None,
out,
}
}
}
pub fn layout_document_positioned(
root: &LayoutBox,
styles: &HashMap<NodeId, Rc<ComputedStyle>>,
fonts: &FontCollection,
page_size: (f32, f32),
) -> (LaidOutBox, Vec<PositionedBox>) {
let mut absolutes = Vec::new();
let mut pos = PosCtx::new(&mut absolutes, page_size);
let laid =
layout_document_from_positioned(root, styles, fonts, page_size.0, 0.0, 0.0, &mut pos);
(laid, absolutes)
}
pub fn layout_document(
root: &LayoutBox,
styles: &HashMap<NodeId, Rc<ComputedStyle>>,
fonts: &FontCollection,
page_width: f32,
) -> LaidOutBox {
layout_document_from(root, styles, fonts, page_width, 0.0, 0.0)
}
pub fn layout_document_from(
root: &LayoutBox,
styles: &HashMap<NodeId, Rc<ComputedStyle>>,
fonts: &FontCollection,
containing_width: f32,
start_x: f32,
start_y: f32,
) -> LaidOutBox {
let mut absolutes = Vec::new();
let mut pos = PosCtx::new(&mut absolutes, (containing_width, 0.0));
layout_document_from_positioned(
root,
styles,
fonts,
containing_width,
start_x,
start_y,
&mut pos,
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn layout_document_from_positioned(
root: &LayoutBox,
styles: &HashMap<NodeId, Rc<ComputedStyle>>,
fonts: &FontCollection,
containing_width: f32,
start_x: f32,
start_y: f32,
pos: &mut PosCtx,
) -> LaidOutBox {
let mut float_ctx = FloatContext::new();
layout_box(
root,
styles,
fonts,
containing_width,
&mut float_ctx,
start_x,
start_y,
pos,
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn layout_box(
b: &LayoutBox,
styles: &HashMap<NodeId, Rc<ComputedStyle>>,
fonts: &FontCollection,
containing_width: f32,
float_ctx: &mut FloatContext,
x: f32,
y: f32,
pos: &mut PosCtx,
) -> LaidOutBox {
layout_box_impl(
b,
styles,
fonts,
containing_width,
None,
None,
float_ctx,
x,
y,
pos,
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn layout_box_with_forced_width(
b: &LayoutBox,
styles: &HashMap<NodeId, Rc<ComputedStyle>>,
fonts: &FontCollection,
containing_width: f32,
forced_content_width: f32,
float_ctx: &mut FloatContext,
x: f32,
y: f32,
pos: &mut PosCtx,
) -> LaidOutBox {
layout_box_impl(
b,
styles,
fonts,
containing_width,
Some(forced_content_width),
None,
float_ctx,
x,
y,
pos,
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn layout_box_with_forced_size(
b: &LayoutBox,
styles: &HashMap<NodeId, Rc<ComputedStyle>>,
fonts: &FontCollection,
containing_width: f32,
forced_content_width: f32,
forced_content_height: f32,
float_ctx: &mut FloatContext,
x: f32,
y: f32,
pos: &mut PosCtx,
) -> LaidOutBox {
layout_box_impl(
b,
styles,
fonts,
containing_width,
Some(forced_content_width),
Some(forced_content_height),
float_ctx,
x,
y,
pos,
)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn measure_box_with_forced_width(
b: &LayoutBox,
styles: &HashMap<NodeId, Rc<ComputedStyle>>,
fonts: &FontCollection,
containing_width: f32,
forced_content_width: f32,
float_ctx: &mut FloatContext,
x: f32,
y: f32,
) -> LaidOutBox {
let mut sink = Vec::new();
let mut pos = PosCtx::new(&mut sink, (0.0, 0.0));
layout_box_with_forced_width(
b,
styles,
fonts,
containing_width,
forced_content_width,
float_ctx,
x,
y,
&mut pos,
)
}
fn layout_out_of_flow_child(
child: &LayoutBox,
child_style: &ComputedStyle,
styles: &HashMap<NodeId, Rc<ComputedStyle>>,
fonts: &FontCollection,
pos: &mut PosCtx,
) {
let (cb_rect, kind) = if child_style.position == Position::Fixed {
(
Rect {
x: 0.0,
y: 0.0,
width: pos.page_size.0,
height: pos.page_size.1,
},
PositionedKind::Fixed,
)
} else {
match pos.abs_cb {
AbsCB::InitialPage => (
Rect {
x: 0.0,
y: 0.0,
width: pos.page_size.0,
height: pos.page_size.1,
},
PositionedKind::AbsoluteInitial,
),
AbsCB::Ancestor { node, rect, .. } => (
rect,
PositionedKind::AbsoluteAncestor {
node,
padding_box_origin: (rect.x, rect.y),
},
),
}
};
let padding = resolve_padding(child_style, cb_rect.width);
let border = resolve_border(child_style);
let margin_left = resolve_lpa_or_zero(child_style.margin_left, cb_rect.width);
let margin_right = resolve_lpa_or_zero(child_style.margin_right, cb_rect.width);
let non_content_width =
margin_left + border.left + padding.left + padding.right + border.right + margin_right;
let has_left = !matches!(child_style.left, LengthPercentageOrAuto::Auto);
let has_right = !matches!(child_style.right, LengthPercentageOrAuto::Auto);
let left = resolve_lpa_or_zero(child_style.left, cb_rect.width);
let right = resolve_lpa_or_zero(child_style.right, cb_rect.width);
let content_width = match child_style.width {
LengthPercentageOrAuto::LengthPercentage(lp) => {
let w = resolve_lp(lp, cb_rect.width);
if child_style.box_sizing == BoxSizing::BorderBox {
(w - padding.left - padding.right - border.left - border.right).max(0.0)
} else {
w
}
}
LengthPercentageOrAuto::Auto if has_left && has_right => {
(cb_rect.width - left - right - non_content_width).max(0.0)
}
LengthPercentageOrAuto::Auto => {
let avail = (cb_rect.width - non_content_width).max(0.0);
aspect_ratio_width(child_style, &padding, &border).unwrap_or_else(|| {
shrink_to_fit_content_width(child, styles, fonts, child_style, avail)
})
}
};
let content_width = clamp_used_width(
child_style,
cb_rect.width,
padding.left + padding.right,
border.left + border.right,
content_width,
);
let margin_box_width = non_content_width + content_width;
let margin_box_x = if has_left {
cb_rect.x + left
} else if has_right {
cb_rect.x + cb_rect.width - margin_box_width - right
} else {
cb_rect.x
};
let has_top = !matches!(child_style.top, LengthPercentageOrAuto::Auto);
let has_bottom = !matches!(child_style.bottom, LengthPercentageOrAuto::Auto);
let top = resolve_lpa_or_zero(child_style.top, cb_rect.height);
let bottom = resolve_lpa_or_zero(child_style.bottom, cb_rect.height);
let margin_box_y = cb_rect.y + if has_top { top } else { 0.0 };
let saved_cb_height = pos.cb_height;
pos.cb_height = if child_style.position == Position::Fixed {
(pos.page_size.1 > 0.0).then_some(pos.page_size.1)
} else {
match pos.abs_cb {
AbsCB::InitialPage => (pos.page_size.1 > 0.0).then_some(pos.page_size.1),
AbsCB::Ancestor { height, .. } => height,
}
};
let mut float_ctx = FloatContext::new();
let mut laid = layout_box_with_forced_width(
child,
styles,
fonts,
cb_rect.width,
content_width,
&mut float_ctx,
margin_box_x,
margin_box_y,
pos,
);
pos.cb_height = saved_cb_height;
if !has_top && has_bottom && cb_rect.height > 0.0 {
let mbh = laid.layout.margin_box_height();
let target_y = cb_rect.y + cb_rect.height - mbh - bottom;
shift_box_y_in_place(&mut laid, margin_box_y - target_y);
}
pos.out.push(PositionedBox { laid, kind });
}
pub(super) fn shrink_to_fit_content_width(
b: &LayoutBox,
styles: &HashMap<NodeId, Rc<ComputedStyle>>,
fonts: &FontCollection,
style: &ComputedStyle,
available_width: f32,
) -> f32 {
let _ = style;
let natural = super::table::measure_natural_content_width(b, styles, fonts);
if natural <= available_width {
return natural.max(0.0);
}
clear_word_width_cache();
let min_content = super::table::measure_min_content_width(b, styles, fonts);
clear_word_width_cache();
available_width.max(min_content.min(natural)).max(0.0)
}
fn resolve_box_geometry(
b: &LayoutBox,
styles: &HashMap<NodeId, Rc<ComputedStyle>>,
fonts: &FontCollection,
containing_width: f32,
forced_content_width: Option<f32>,
cb_height: Option<f32>,
) -> (ComputedStyle, EdgeSizes, EdgeSizes, EdgeSizes, f32) {
let mut style = box_style(b, styles).into_owned();
resolve_percentage_heights(&mut style, cb_height);
if let BoxContent::Image(image_content) = &b.content {
apply_replaced_element_auto_size(&mut style, image_content, containing_width);
}
let padding = resolve_padding(&style, containing_width);
let border = resolve_border(&style);
let (content_width, margin_left, margin_right) = match forced_content_width {
Some(w) => (
w,
resolve_lpa_or_zero(style.margin_left, containing_width),
resolve_lpa_or_zero(style.margin_right, containing_width),
),
None if style.float != Float::None
&& !matches!(style.width, LengthPercentageOrAuto::Auto) =>
{
let width = resolve_lpa_or_zero(style.width, containing_width);
let width = if style.box_sizing == BoxSizing::BorderBox {
(width - padding.left - padding.right - border.left - border.right).max(0.0)
} else {
width
};
(
clamp_used_width(
&style,
containing_width,
padding.left + padding.right,
border.left + border.right,
width,
),
resolve_lpa_or_zero(style.margin_left, containing_width),
resolve_lpa_or_zero(style.margin_right, containing_width),
)
}
None if style.float != Float::None => {
let available = (containing_width
- resolve_lpa_or_zero(style.margin_left, containing_width)
- resolve_lpa_or_zero(style.margin_right, containing_width)
- padding.left
- padding.right
- border.left
- border.right)
.max(0.0);
let width = aspect_ratio_width(&style, &padding, &border).unwrap_or_else(|| {
shrink_to_fit_content_width(b, styles, fonts, &style, available)
});
(
clamp_used_width(
&style,
containing_width,
padding.left + padding.right,
border.left + border.right,
width,
),
resolve_lpa_or_zero(style.margin_left, containing_width),
resolve_lpa_or_zero(style.margin_right, containing_width),
)
}
None => resolve_width_and_horizontal_margins(
&style,
containing_width,
padding.left + padding.right,
border.left + border.right,
),
};
let margin = EdgeSizes {
top: resolve_lpa_or_zero(style.margin_top, containing_width),
right: margin_right,
bottom: resolve_lpa_or_zero(style.margin_bottom, containing_width),
left: margin_left,
};
(style, padding, border, margin, content_width)
}
#[allow(clippy::too_many_arguments)]
fn layout_box_impl(
b: &LayoutBox,
styles: &HashMap<NodeId, Rc<ComputedStyle>>,
fonts: &FontCollection,
containing_width: f32,
forced_content_width: Option<f32>,
forced_content_height: Option<f32>,
float_ctx: &mut FloatContext,
x: f32,
y: f32,
pos: &mut PosCtx,
) -> LaidOutBox {
let (style, padding, border, mut margin, mut content_width) = resolve_box_geometry(
b,
styles,
fonts,
containing_width,
forced_content_width,
pos.cb_height,
);
let content_x = x + margin.left + border.left + padding.left;
let mut content_y = y + margin.top + border.top + padding.top;
let (offset_x, offset_y) = match RelativeInset::of(&style) {
Some(inset) => resolve_relative_offset(&inset, content_width),
None => (0.0, 0.0),
};
let saved_cb = pos.abs_cb;
let saved_cb_height = pos.cb_height;
let own_definite_height = resolve_definite_content_height(&style, &padding, &border);
if style.position != Position::Static {
if let Some(node) = b.node {
pos.abs_cb = AbsCB::Ancestor {
node,
rect: Rect {
x: content_x - padding.left + offset_x,
y: content_y - padding.top + offset_y,
width: padding.left + content_width + padding.right,
height: 0.0,
},
height: own_definite_height.map(|h| h + padding.top + padding.bottom),
};
}
}
pos.cb_height = match &b.content {
BoxContent::Blocks(_) | BoxContent::Inline(_) if b.node.is_none() => saved_cb_height,
BoxContent::Blocks(_) | BoxContent::Inline(_) => own_definite_height,
_ => None,
};
let (mut content, content_height) = match &b.content {
BoxContent::Blocks(children) => {
let mut cursor_y = content_y;
let mut max_float_bottom = content_y;
let mut laid_children: Vec<LaidOutBox> = Vec::with_capacity(children.len());
for child in children {
let child_style = box_style(child, styles);
if child_style.position.is_out_of_flow() {
layout_out_of_flow_child(child, &child_style, styles, fonts, pos);
continue;
}
if child_style.clear != Clear::None {
cursor_y = float_ctx.clearance(child_style.clear, cursor_y);
}
if child_style.float != Float::None {
let child_laid = layout_float_child(
child,
&child_style,
styles,
fonts,
content_width,
float_ctx,
content_x,
cursor_y,
pos,
);
let float_top = child_laid.layout.content.y
- child_laid.layout.padding.top
- child_laid.layout.border.top
- child_laid.layout.margin.top;
max_float_bottom =
max_float_bottom.max(float_top + child_laid.layout.margin_box_height());
laid_children.push(child_laid);
continue;
}
let child_margin_top = resolve_lpa_or_zero(child_style.margin_top, content_width);
if let Some(prev) = laid_children.iter().rev().find(|c| !c.is_float) {
let prev_margin_bottom = prev.layout.margin.bottom;
let collapsed = collapse_adjacent_margins(prev_margin_bottom, child_margin_top);
cursor_y -= prev_margin_bottom + child_margin_top - collapsed;
}
let child_laid = layout_box(
child,
styles,
fonts,
content_width,
float_ctx,
content_x,
cursor_y,
pos,
);
cursor_y += child_laid.layout.margin_box_height();
laid_children.push(child_laid);
}
let auto_height = cursor_y.max(max_float_bottom) - content_y;
let height = resolve_used_height(&style, &padding, &border, content_width, auto_height);
(LaidOutContent::Blocks(laid_children), height)
}
BoxContent::Inline(spans) => {
let mut lines = layout_inline_content(
spans,
styles,
fonts,
content_width,
content_x,
content_y,
Some(&*float_ctx),
b.node.is_some().then_some(&style),
pos,
);
place_atomic_inlines(&mut lines);
apply_text_overflow(&mut lines, &style, content_width, fonts);
let lines_height: f32 = lines.iter().map(|line| line.rect.height).sum();
let height =
resolve_used_height(&style, &padding, &border, content_width, lines_height);
(LaidOutContent::Inline(lines), height)
}
BoxContent::Table(table) => {
let (h_spacing, v_spacing) = if style.border_collapse == BorderCollapse::Collapse {
(0.0, 0.0)
} else {
(
style.border_spacing_horizontal.0,
style.border_spacing_vertical.0,
)
};
let (laid_table, table_height, table_width) = layout_table(
table,
styles,
fonts,
content_width,
style.table_layout,
h_spacing,
v_spacing,
content_x,
content_y,
pos,
);
let height =
resolve_used_height(&style, &padding, &border, content_width, table_height);
content_width = content_width.max(table_width);
(LaidOutContent::Table(laid_table), height)
}
BoxContent::Grid(grid) => {
let (laid_grid, grid_height) = layout_grid(
grid,
styles,
fonts,
&style,
content_width,
content_x,
content_y,
pos,
);
let height = resolve_used_height(&style, &padding, &border, content_width, grid_height);
(LaidOutContent::Grid(laid_grid), height)
}
BoxContent::Flex(flex) => {
let (items, flex_height) = layout_flex(
flex,
styles,
fonts,
&style,
content_width,
content_x,
content_y,
pos,
);
let height = resolve_used_height(&style, &padding, &border, content_width, flex_height);
(LaidOutContent::Flex(items), height)
}
BoxContent::Image(image_content) => {
let height = resolve_used_height(&style, &padding, &border, content_width, 0.0);
(LaidOutContent::Image(image_content.image.clone()), height)
}
};
pos.abs_cb = saved_cb;
pos.cb_height = saved_cb_height;
let mut content_height = forced_content_height.unwrap_or(content_height);
let height_is_auto =
forced_content_height.is_none() && matches!(style.height, LengthPercentageOrAuto::Auto);
apply_margin_collapse(
&mut content,
&mut content_height,
&mut margin,
&mut content_y,
&border,
&padding,
height_is_auto,
);
let marker = b.marker.as_deref().and_then(|text| {
layout_list_marker(text, &style, fonts, content_x, content_y).map(Box::new)
});
let mut laid = LaidOutBox {
node: b.node,
layout: Layout {
content: Rect {
x: content_x,
y: content_y,
width: content_width,
height: content_height,
},
padding,
border,
margin,
fragment: FragmentPosition::Whole,
},
fragmentation: FragmentationHints::from(&style),
has_visible_decoration: has_visible_decoration(&style, &border),
is_float: style.float != Float::None,
content,
marker,
link: b.link.clone(),
};
if offset_x != 0.0 {
shift_box_x_in_place(&mut laid, offset_x);
}
if offset_y != 0.0 {
shift_box_y_in_place(&mut laid, -offset_y);
}
laid
}
fn layout_list_marker(
text: &str,
style: &ComputedStyle,
fonts: &FontCollection,
content_x: f32,
content_y: f32,
) -> Option<LineBox> {
let first_char = text.chars().next()?;
let font_index = fonts.select_for_char(
&style.font_family,
style.font_weight,
style.font_style,
first_char,
)?;
let run = shape_run(text, font_index, fonts, style);
let width = run.width;
let height = run.line_height;
Some(finish_line(
vec![run],
Vec::new(),
width,
content_x - LIST_MARKER_GAP - width,
content_y,
height,
fonts,
))
}
#[allow(clippy::too_many_arguments)]
fn layout_float_child(
child: &LayoutBox,
child_style: &ComputedStyle,
styles: &HashMap<NodeId, Rc<ComputedStyle>>,
fonts: &FontCollection,
containing_width: f32,
float_ctx: &mut FloatContext,
containing_left: f32,
preferred_top: f32,
pos: &mut PosCtx,
) -> LaidOutBox {
let (_, padding, border, margin, child_content_width) =
resolve_box_geometry(child, styles, fonts, containing_width, None, pos.cb_height);
let margin_box_width = margin.left
+ border.left
+ padding.left
+ child_content_width
+ padding.right
+ border.right
+ margin.right;
let (float_x, float_y) = float_ctx.place(
child_style.float,
preferred_top,
containing_left,
containing_left + containing_width,
margin_box_width,
);
let child_laid = layout_box(
child,
styles,
fonts,
containing_width,
float_ctx,
float_x,
float_y,
pos,
);
float_ctx.register(
child_style.float,
float_x,
float_y,
margin_box_width,
child_laid.layout.margin_box_height(),
);
child_laid
}
pub(super) fn resolve_relative_offset(inset: &RelativeInset, containing_width: f32) -> (f32, f32) {
let resolve =
|primary: LengthPercentageOrAuto, secondary: LengthPercentageOrAuto, basis: f32| {
match primary {
LengthPercentageOrAuto::LengthPercentage(lp) => resolve_lp(lp, basis),
LengthPercentageOrAuto::Auto => match secondary {
LengthPercentageOrAuto::LengthPercentage(lp) => -resolve_lp(lp, basis),
LengthPercentageOrAuto::Auto => 0.0,
},
}
};
let dx = resolve(inset.left, inset.right, containing_width);
let dy = resolve(inset.top, inset.bottom, 0.0);
(dx, dy)
}
pub(crate) fn has_visible_decoration(style: &ComputedStyle, border: &EdgeSizes) -> bool {
if style.background_color.alpha > 0.0 {
return true;
}
if style.background_image.is_some() {
return true;
}
[
(border.top, style.border_top_style),
(border.right, style.border_right_style),
(border.bottom, style.border_bottom_style),
(border.left, style.border_left_style),
]
.into_iter()
.any(|(width, border_style)| width > 0.0 && border_style != BorderStyle::None)
}
pub(super) fn box_style<'a>(
b: &LayoutBox,
styles: &'a HashMap<NodeId, Rc<ComputedStyle>>,
) -> Cow<'a, ComputedStyle> {
match b.node {
Some(node) => Cow::Borrowed(&styles[&node]),
None => Cow::Owned(ComputedStyle {
display: Display::Block,
..ComputedStyle::default()
}),
}
}
pub(super) fn resolve_lp(lp: LengthPercentage, basis: f32) -> f32 {
match lp {
LengthPercentage::Length(px) => px,
LengthPercentage::Percentage(fraction) => fraction * basis,
LengthPercentage::Calc { px, percent } => px + percent * basis,
}
}
pub(crate) fn resolve_lpa_or_zero(lpa: LengthPercentageOrAuto, basis: f32) -> f32 {
match lpa {
LengthPercentageOrAuto::Auto => 0.0,
LengthPercentageOrAuto::LengthPercentage(lp) => resolve_lp(lp, basis),
}
}
pub(crate) fn clamp_used_width(
style: &ComputedStyle,
containing_width: f32,
padding_lr: f32,
border_lr: f32,
width: f32,
) -> f32 {
let to_content_box = |v: f32| {
if style.box_sizing == BoxSizing::BorderBox {
(v - padding_lr - border_lr).max(0.0)
} else {
v
}
};
let mut used = width;
if let MaxSize::LengthPercentage(lp) = style.max_width {
used = used.min(to_content_box(resolve_lp(lp, containing_width)));
}
used.max(to_content_box(resolve_lp(
style.min_width,
containing_width,
)))
.max(0.0)
}
pub(super) fn resolve_percentage_heights(style: &mut ComputedStyle, cb_height: Option<f32>) {
let has_percent = |lp: LengthPercentage| match lp {
LengthPercentage::Length(_) => false,
LengthPercentage::Percentage(_) => true,
LengthPercentage::Calc { percent, .. } => percent != 0.0,
};
let resolve = |lp: LengthPercentage| -> Option<LengthPercentage> {
if !has_percent(lp) {
return Some(lp);
}
cb_height.map(|h| LengthPercentage::Length(resolve_lp(lp, h)))
};
if let LengthPercentageOrAuto::LengthPercentage(lp) = style.height {
style.height = match resolve(lp) {
Some(lp) => LengthPercentageOrAuto::LengthPercentage(lp),
None => LengthPercentageOrAuto::Auto,
};
}
style.min_height = resolve(style.min_height).unwrap_or(LengthPercentage::Length(0.0));
if let MaxSize::LengthPercentage(lp) = style.max_height {
style.max_height = match resolve(lp) {
Some(lp) => MaxSize::LengthPercentage(lp),
None => MaxSize::None,
};
}
}
fn resolve_definite_content_height(
style: &ComputedStyle,
padding: &EdgeSizes,
border: &EdgeSizes,
) -> Option<f32> {
let padding_tb = padding.top + padding.bottom;
let border_tb = border.top + border.bottom;
let height = resolve_height(style, padding_tb, border_tb)?;
Some(clamp_used_height(style, padding_tb, border_tb, height))
}
pub(crate) fn clamp_used_height(
style: &ComputedStyle,
padding_tb: f32,
border_tb: f32,
height: f32,
) -> f32 {
let to_content_box = |v: f32| {
if style.box_sizing == BoxSizing::BorderBox {
(v - padding_tb - border_tb).max(0.0)
} else {
v
}
};
let mut used = height;
if let MaxSize::LengthPercentage(lp) = style.max_height {
if let Some(px) = definite_height_px(lp) {
used = used.min(to_content_box(px));
}
}
if let Some(px) = definite_height_px(style.min_height) {
used = used.max(to_content_box(px));
}
used.max(0.0)
}
fn definite_height_px(lp: LengthPercentage) -> Option<f32> {
match lp {
LengthPercentage::Length(px) => Some(px),
LengthPercentage::Percentage(_) => None,
LengthPercentage::Calc { px, percent: 0.0 } => Some(px),
LengthPercentage::Calc { .. } => None,
}
}
pub(crate) fn resolve_padding(style: &ComputedStyle, basis: f32) -> EdgeSizes {
EdgeSizes {
top: resolve_lp(style.padding_top, basis),
right: resolve_lp(style.padding_right, basis),
bottom: resolve_lp(style.padding_bottom, basis),
left: resolve_lp(style.padding_left, basis),
}
}
pub(crate) fn resolve_border(style: &ComputedStyle) -> EdgeSizes {
let width_or_zero = |width: Length, border_style: BorderStyle| {
if border_style == BorderStyle::None {
0.0
} else {
width.0
}
};
EdgeSizes {
top: width_or_zero(style.border_top_width, style.border_top_style),
right: width_or_zero(style.border_right_width, style.border_right_style),
bottom: width_or_zero(style.border_bottom_width, style.border_bottom_style),
left: width_or_zero(style.border_left_width, style.border_left_style),
}
}
pub(crate) fn resolve_used_height(
style: &ComputedStyle,
padding: &EdgeSizes,
border: &EdgeSizes,
content_width: f32,
auto_height: f32,
) -> f32 {
let padding_tb = padding.top + padding.bottom;
let border_tb = border.top + border.bottom;
let height = resolve_height(style, padding_tb, border_tb)
.or_else(|| aspect_ratio_height(style, padding, border, content_width))
.unwrap_or(auto_height);
clamp_used_height(style, padding_tb, border_tb, height)
}
fn aspect_ratio_height(
style: &ComputedStyle,
padding: &EdgeSizes,
border: &EdgeSizes,
content_width: f32,
) -> Option<f32> {
let ratio = style.aspect_ratio.ratio?;
if style.box_sizing == BoxSizing::BorderBox {
let border_box_width =
content_width + padding.left + padding.right + border.left + border.right;
Some(
(border_box_width / ratio - padding.top - padding.bottom - border.top - border.bottom)
.max(0.0),
)
} else {
Some(content_width / ratio)
}
}
pub(crate) fn aspect_ratio_width(
style: &ComputedStyle,
padding: &EdgeSizes,
border: &EdgeSizes,
) -> Option<f32> {
let ratio = style.aspect_ratio.ratio?;
let padding_tb = padding.top + padding.bottom;
let border_tb = border.top + border.bottom;
let content_height = resolve_height(style, padding_tb, border_tb)?;
if style.box_sizing == BoxSizing::BorderBox {
let border_box_height = content_height + padding_tb + border_tb;
Some(
(border_box_height * ratio - padding.left - padding.right - border.left - border.right)
.max(0.0),
)
} else {
Some(content_height * ratio)
}
}
fn resolve_height(style: &ComputedStyle, padding_tb: f32, border_tb: f32) -> Option<f32> {
let LengthPercentageOrAuto::LengthPercentage(lp) = style.height else {
return None;
};
let px = definite_height_px(lp)?;
Some(if style.box_sizing == BoxSizing::BorderBox {
(px - padding_tb - border_tb).max(0.0)
} else {
px
})
}
pub(super) fn apply_replaced_element_auto_size(
style: &mut ComputedStyle,
image: &ImageBoxContent,
containing_width: f32,
) {
if style.aspect_ratio.auto {
if let Some(ratio) = intrinsic_ratio(image) {
style.aspect_ratio.ratio = Some(ratio);
}
}
let width_is_auto = matches!(style.width, LengthPercentageOrAuto::Auto);
let height_is_auto = matches!(style.height, LengthPercentageOrAuto::Auto);
let padding = resolve_padding(style, containing_width);
let border = resolve_border(style);
if !width_is_auto {
return;
}
if !height_is_auto {
if let Some(width) = aspect_ratio_width(style, &padding, &border) {
style.width = LengthPercentageOrAuto::LengthPercentage(LengthPercentage::Length(width));
}
return;
}
let attr_size = (
image.attr_width.map(|w| w as f32),
image.attr_height.map(|h| h as f32),
);
let intrinsic_size = image
.image
.as_ref()
.map(|prepared| (prepared.width, prepared.height));
let (width, height) = match attr_size {
(Some(w), Some(h)) => (w, h),
(Some(w), None) => (
w,
derive_via_aspect_ratio(w, intrinsic_size.map(|(iw, ih)| (ih, iw))),
),
(None, Some(h)) => (derive_via_aspect_ratio(h, intrinsic_size), h),
(None, None) => intrinsic_size.unwrap_or((0.0, 0.0)),
};
let height = if style.aspect_ratio.auto {
height
} else {
aspect_ratio_height(style, &padding, &border, width).unwrap_or(height)
};
let (width, height) = if attr_size == (None, None) {
let (w, h) =
constrain_replaced_size(style, containing_width, &padding, &border, width, height);
if style.box_sizing == BoxSizing::BorderBox {
(
w + padding.left + padding.right + border.left + border.right,
h + padding.top + padding.bottom + border.top + border.bottom,
)
} else {
(w, h)
}
} else {
(width, height)
};
style.width = LengthPercentageOrAuto::LengthPercentage(LengthPercentage::Length(width));
style.height = LengthPercentageOrAuto::LengthPercentage(LengthPercentage::Length(height));
}
pub(super) fn replaced_auto_content_size(
style: &ComputedStyle,
image: &ImageBoxContent,
containing_width: f32,
) -> Option<(f32, f32)> {
if !matches!(style.width, LengthPercentageOrAuto::Auto)
|| !matches!(style.height, LengthPercentageOrAuto::Auto)
{
return None;
}
let mut style = style.clone();
apply_replaced_element_auto_size(&mut style, image, containing_width);
let width = resolve_lpa_or_zero(style.width, containing_width);
let height = resolve_lpa_or_zero(style.height, containing_width);
if style.box_sizing == BoxSizing::BorderBox {
let padding = resolve_padding(&style, containing_width);
let border = resolve_border(&style);
Some((
(width - padding.left - padding.right - border.left - border.right).max(0.0),
(height - padding.top - padding.bottom - border.top - border.bottom).max(0.0),
))
} else {
Some((width, height))
}
}
fn constrain_replaced_size(
style: &ComputedStyle,
containing_width: f32,
padding: &EdgeSizes,
border: &EdgeSizes,
width: f32,
height: f32,
) -> (f32, f32) {
if width <= 0.0 || height <= 0.0 {
return (width, height);
}
let padding_lr = padding.left + padding.right;
let border_lr = border.left + border.right;
let padding_tb = padding.top + padding.bottom;
let border_tb = border.top + border.bottom;
let border_box = style.box_sizing == BoxSizing::BorderBox;
let to_content_w = |v: f32| {
if border_box {
(v - padding_lr - border_lr).max(0.0)
} else {
v
}
};
let to_content_h = |v: f32| {
if border_box {
(v - padding_tb - border_tb).max(0.0)
} else {
v
}
};
let min_w = to_content_w(resolve_lp(style.min_width, containing_width));
let max_w = match style.max_width {
MaxSize::LengthPercentage(lp) => to_content_w(resolve_lp(lp, containing_width)),
MaxSize::None => f32::INFINITY,
}
.max(min_w);
let min_h = definite_height_px(style.min_height)
.map(to_content_h)
.unwrap_or(0.0);
let max_h = match style.max_height {
MaxSize::LengthPercentage(lp) => definite_height_px(lp).map(to_content_h),
MaxSize::None => None,
}
.unwrap_or(f32::INFINITY)
.max(min_h);
let (w, h) = (width, height);
match (w > max_w, w < min_w, h > max_h, h < min_h) {
(true, _, true, _) if max_w / w <= max_h / h => (max_w, (max_w * h / w).max(min_h)),
(true, _, true, _) => ((max_h * w / h).max(min_w), max_h),
(_, true, _, true) if min_w / w <= min_h / h => ((min_h * w / h).min(max_w), min_h),
(_, true, _, true) => (min_w, (min_w * h / w).min(max_h)),
(_, true, true, _) => (min_w, max_h),
(true, _, _, true) => (max_w, min_h),
(true, _, _, _) => (max_w, (max_w * h / w).max(min_h)),
(_, true, _, _) => (min_w, (min_w * h / w).min(max_h)),
(_, _, true, _) => ((max_h * w / h).max(min_w), max_h),
(_, _, _, true) => ((min_h * w / h).min(max_w), min_h),
_ => (w, h),
}
}
fn intrinsic_ratio(image: &ImageBoxContent) -> Option<f32> {
let prepared = image.image.as_ref()?;
(prepared.height > 0.0).then(|| prepared.width / prepared.height)
}
fn derive_via_aspect_ratio(known: f32, ratio_basis: Option<(f32, f32)>) -> f32 {
match ratio_basis {
Some((other_intrinsic, known_intrinsic)) if known_intrinsic > 0.0 => {
known * other_intrinsic / known_intrinsic
}
_ => 0.0,
}
}
fn apply_margin_collapse(
content: &mut LaidOutContent,
content_height: &mut f32,
margin: &mut EdgeSizes,
content_y: &mut f32,
border: &EdgeSizes,
padding: &EdgeSizes,
height_is_auto: bool,
) {
let content_is_empty = match content {
LaidOutContent::Blocks(children) | LaidOutContent::Flex(children) => children.is_empty(),
LaidOutContent::Inline(lines) => lines.is_empty(),
LaidOutContent::Grid(grid) => grid.rows.iter().all(|row| row.items.is_empty()),
LaidOutContent::Table(_) | LaidOutContent::Image(_) => false,
};
let is_empty_block = *content_height == 0.0
&& border.top == 0.0
&& border.bottom == 0.0
&& padding.top == 0.0
&& padding.bottom == 0.0
&& content_is_empty;
if is_empty_block {
let collapsed = collapse_adjacent_margins(margin.top, margin.bottom);
margin.top = collapsed;
margin.bottom = 0.0;
return;
}
let LaidOutContent::Blocks(children) = content else {
return;
};
if border.top == 0.0 && padding.top == 0.0 && height_is_auto {
if let Some(first_top) = children
.iter()
.find(|c| !c.is_float)
.map(|c| c.layout.margin.top)
{
let effective = collapse_adjacent_margins(margin.top, first_top);
let child_delta = effective - first_top - margin.top;
for child in children.iter_mut() {
shift_box_y_in_place(child, -child_delta);
}
*content_y += effective - margin.top;
*content_height -= first_top;
margin.top = effective;
}
}
if border.bottom == 0.0 && padding.bottom == 0.0 && height_is_auto {
if let Some(last_bottom) = children
.iter()
.rev()
.find(|c| !c.is_float)
.map(|c| c.layout.margin.bottom)
{
let effective = collapse_adjacent_margins(margin.bottom, last_bottom);
*content_height -= last_bottom;
margin.bottom = effective;
}
}
}
fn collapse_adjacent_margins(a: f32, b: f32) -> f32 {
let positive = a.max(0.0).max(b.max(0.0));
let negative = a.min(0.0).min(b.min(0.0));
positive + negative
}
pub(crate) fn resolve_width_and_horizontal_margins(
style: &ComputedStyle,
containing_width: f32,
padding_lr: f32,
border_lr: f32,
) -> (f32, f32, f32) {
let (width, margin_left, margin_right) =
solve_horizontal(style, containing_width, padding_lr, border_lr, None);
let clamped = clamp_used_width(style, containing_width, padding_lr, border_lr, width);
if clamped == width {
return (width, margin_left, margin_right);
}
solve_horizontal(
style,
containing_width,
padding_lr,
border_lr,
Some(clamped),
)
}
fn solve_horizontal(
style: &ComputedStyle,
containing_width: f32,
padding_lr: f32,
border_lr: f32,
used_width: Option<f32>,
) -> (f32, f32, f32) {
let margin_left_is_auto = matches!(style.margin_left, LengthPercentageOrAuto::Auto);
let margin_right_is_auto = matches!(style.margin_right, LengthPercentageOrAuto::Auto);
let specified_width = match used_width {
Some(w) => Some(w),
None if matches!(style.width, LengthPercentageOrAuto::Auto) => None,
None => {
let width = resolve_lpa_or_zero(style.width, containing_width);
Some(if style.box_sizing == BoxSizing::BorderBox {
(width - padding_lr - border_lr).max(0.0)
} else {
width
})
}
};
let Some(width) = specified_width else {
let margin_left = resolve_lpa_or_zero(style.margin_left, containing_width);
let margin_right = resolve_lpa_or_zero(style.margin_right, containing_width);
let width =
(containing_width - margin_left - border_lr - padding_lr - margin_right).max(0.0);
return (width, margin_left, margin_right);
};
let remaining = (containing_width - border_lr - padding_lr - width).max(0.0);
match (margin_left_is_auto, margin_right_is_auto) {
(true, true) => {
let half = remaining / 2.0;
(width, half, half)
}
(true, false) => {
let margin_right = resolve_lpa_or_zero(style.margin_right, containing_width);
(width, (remaining - margin_right).max(0.0), margin_right)
}
(false, true) => {
let margin_left = resolve_lpa_or_zero(style.margin_left, containing_width);
(width, margin_left, (remaining - margin_left).max(0.0))
}
(false, false) => {
let margin_left = resolve_lpa_or_zero(style.margin_left, containing_width);
let margin_right = containing_width - border_lr - padding_lr - width - margin_left;
(width, margin_left, margin_right)
}
}
}
fn place_atomic_inlines(lines: &mut [LineBox]) {
for line in lines.iter_mut() {
let baseline_y = line.rect.y + line.baseline;
for atomic in line.atomics.iter_mut() {
let target_x = line.rect.x + atomic.x_offset;
let target_y = baseline_y - atomic.baseline_shift - atomic.margin_box_height
+ atomic.baseline_from_bottom;
let layout = atomic.content.layout;
let current_x =
layout.content.x - layout.padding.left - layout.border.left + -layout.margin.left;
let current_y =
layout.content.y - layout.padding.top - layout.border.top - layout.margin.top;
shift_box_y_in_place(&mut atomic.content, current_y - target_y);
shift_box_x_in_place(&mut atomic.content, target_x - current_x);
}
}
}
pub(super) fn shift_box_x(b: &LaidOutBox, delta: f32) -> LaidOutBox {
let mut shifted = b.clone();
shift_box_x_in_place(&mut shifted, delta);
shifted
}
pub(super) fn shift_box_x_in_place(b: &mut LaidOutBox, delta: f32) {
b.layout.content.x += delta;
if let Some(marker) = &mut b.marker {
marker.rect.x += delta;
}
match &mut b.content {
LaidOutContent::Blocks(children) | LaidOutContent::Flex(children) => {
for child in children.iter_mut() {
shift_box_x_in_place(child, delta);
}
}
LaidOutContent::Grid(grid) => {
for row in grid.rows.iter_mut() {
for item in row.items.iter_mut() {
shift_box_x_in_place(item, delta);
}
}
}
LaidOutContent::Inline(lines) => {
for line in lines.iter_mut() {
line.rect.x += delta;
for atomic in line.atomics.iter_mut() {
shift_box_x_in_place(&mut atomic.content, delta);
}
}
}
LaidOutContent::Table(table) => {
if let Some(caption) = &mut table.caption {
shift_box_x_in_place(caption, delta);
}
for row in table.rows.iter_mut() {
for cell in row.cells.iter_mut() {
shift_box_x_in_place(cell, delta);
}
}
}
LaidOutContent::Image(_) => {}
}
}
pub(super) fn shift_box_y(b: &LaidOutBox, delta: f32) -> LaidOutBox {
let mut shifted = b.clone();
shift_box_y_in_place(&mut shifted, delta);
shifted
}
pub(super) fn shift_box_y_in_place(b: &mut LaidOutBox, delta: f32) {
shift_rect_y(&mut b.layout.content, delta);
if let Some(marker) = &mut b.marker {
shift_rect_y(&mut marker.rect, delta);
}
match &mut b.content {
LaidOutContent::Blocks(children) | LaidOutContent::Flex(children) => {
for child in children.iter_mut() {
shift_box_y_in_place(child, delta);
}
}
LaidOutContent::Grid(grid) => {
for row in grid.rows.iter_mut() {
row.top -= delta;
row.bottom -= delta;
for item in row.items.iter_mut() {
shift_box_y_in_place(item, delta);
}
}
}
LaidOutContent::Inline(lines) => {
for line in lines.iter_mut() {
shift_rect_y(&mut line.rect, delta);
for atomic in line.atomics.iter_mut() {
shift_box_y_in_place(&mut atomic.content, delta);
}
}
}
LaidOutContent::Table(table) => {
if let Some(caption) = &mut table.caption {
shift_box_y_in_place(caption, delta);
}
for row in table.rows.iter_mut() {
for cell in row.cells.iter_mut() {
shift_box_y_in_place(cell, delta);
}
}
}
LaidOutContent::Image(_) => {}
}
}
fn shift_rect_y(rect: &mut Rect, delta: f32) {
rect.y -= delta;
}
pub(super) fn shift_content_vertical(b: &LaidOutBox, delta: f32) -> LaidOutBox {
let mut b = b.clone();
match &mut b.content {
LaidOutContent::Blocks(children) | LaidOutContent::Flex(children) => {
for child in children.iter_mut() {
*child = shift_box_y(child, delta);
}
}
LaidOutContent::Grid(grid) => {
for row in grid.rows.iter_mut() {
row.top -= delta;
row.bottom -= delta;
for item in row.items.iter_mut() {
*item = shift_box_y(item, delta);
}
}
}
LaidOutContent::Inline(lines) => {
for line in lines.iter_mut() {
shift_rect_y(&mut line.rect, delta);
for atomic in line.atomics.iter_mut() {
atomic.content = shift_box_y(&atomic.content, delta);
}
}
}
LaidOutContent::Table(table) => {
if let Some(caption) = &mut table.caption {
**caption = shift_box_y(caption, delta);
}
for row in table.rows.iter_mut() {
for cell in row.cells.iter_mut() {
*cell = shift_box_y(cell, delta);
}
}
}
LaidOutContent::Image(_) => {}
}
b
}
#[cfg(test)]
mod tests {
use super::super::box_tree::build_box_tree;
use super::*;
use crate::fonts::Font;
use crate::html::{self, Dom, NodeData};
use crate::pdf::{ImagePlane, PlaneColorSpace};
use crate::style::{compute_styles, parse_stylesheet, user_agent_stylesheet, Stylesheet};
const TEST_FONT_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fonts/DejaVuSans.ttf");
fn test_fonts() -> FontCollection {
FontCollection::new(vec![
Font::load(TEST_FONT_PATH).expect("should load bundled test font")
])
}
fn find_all(dom: &Dom, id: NodeId, tag: &str, out: &mut Vec<NodeId>) {
if let NodeData::Element { name, .. } = &dom.node(id).data {
if &*name.local == tag {
out.push(id);
}
}
for child in dom.children(id) {
find_all(dom, child, tag, out);
}
}
fn find_box(b: &LayoutBox, target: NodeId) -> Option<&LayoutBox> {
if b.node == Some(target) {
return Some(b);
}
if let BoxContent::Blocks(children) = &b.content {
for child in children {
if let Some(found) = find_box(child, target) {
return Some(found);
}
}
}
None
}
fn find_laid_out(b: &LaidOutBox, target: NodeId) -> Option<&LaidOutBox> {
if b.node == Some(target) {
return Some(b);
}
if let LaidOutContent::Blocks(children) = &b.content {
for child in children {
if let Some(found) = find_laid_out(child, target) {
return Some(found);
}
}
}
None
}
#[test]
fn display_none_excludes_element_and_subtree() {
let dom = html::parse(
br#"<div><p class="hidden">hidden</p><p class="visible">visible</p></div>"#,
);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(".hidden { display: none; }");
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let mut ps = Vec::new();
find_all(&dom, dom.document(), "p", &mut ps);
let (hidden_p, visible_p) = (ps[0], ps[1]);
assert!(find_box(&tree, hidden_p).is_none());
assert!(find_box(&tree, visible_p).is_some());
}
#[test]
fn mixed_block_and_inline_children_get_anonymous_block_wrapping() {
let dom = html::parse(br#"<div class="outer">before <p>P</p> after</div>"#);
let ua = user_agent_stylesheet();
let author = Stylesheet::default();
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let mut ps = Vec::new();
find_all(&dom, dom.document(), "p", &mut ps);
let div_box = find_box(&tree, divs[0]).expect("div box not found");
let BoxContent::Blocks(children) = &div_box.content else {
panic!("expected block container")
};
assert_eq!(children.len(), 3, "before-text / <p> / after-text");
let joined_text = |content: &BoxContent| match content {
BoxContent::Inline(spans) => spans.iter().map(|s| s.text.as_str()).collect::<String>(),
BoxContent::Blocks(_)
| BoxContent::Table(_)
| BoxContent::Flex(_)
| BoxContent::Grid(_)
| BoxContent::Image(_) => {
panic!("expected inline content")
}
};
assert_eq!(joined_text(&children[0].content).trim(), "before");
assert_eq!(children[1].node, Some(ps[0]));
assert_eq!(joined_text(&children[2].content).trim(), "after");
}
#[test]
fn auto_width_fills_containing_block_minus_margins() {
let dom = html::parse(br#"<div class="box"></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(".box { margin: 10px; }");
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let div_box = find_laid_out(&laid, divs[0]).expect("div box not found");
assert_eq!(div_box.layout.margin.left, 10.0);
assert_eq!(div_box.layout.content.width, 764.0);
assert_eq!(div_box.layout.content.x, 18.0);
}
#[test]
fn auto_margins_center_element_with_explicit_width() {
let dom = html::parse(br#"<div class="centered"></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(".centered { width: 400px; margin: 0 auto; }");
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let div_box = find_laid_out(&laid, divs[0]).expect("div box not found");
assert_eq!(div_box.layout.content.width, 400.0);
assert_eq!(div_box.layout.margin.left, div_box.layout.margin.right);
assert_eq!(div_box.layout.margin.left, 192.0);
}
#[test]
fn over_constrained_box_recalculates_margin_right_to_fit_the_containing_block() {
let dom = html::parse(br#"<div class="box"></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(".box { width: 300px; margin: 0 50px 0 50px; }");
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let div_box = find_laid_out(&laid, divs[0]).expect("div box not found");
assert_eq!(div_box.layout.content.width, 300.0);
assert_eq!(div_box.layout.margin.left, 50.0);
assert_eq!(
div_box.layout.margin.right, 434.0,
"over-constrained margin-right should be recalculated, not the specified 50px"
);
}
#[test]
fn over_constrained_recalculation_can_produce_a_negative_margin_right() {
let dom = html::parse(br#"<div class="box"></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(".box { width: 784px; margin: 0 99px 0 30px; }");
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let div_box = find_laid_out(&laid, divs[0]).expect("div box not found");
assert_eq!(div_box.layout.margin.right, -30.0);
}
#[test]
fn block_siblings_stack_vertically_by_content_height() {
let dom = html::parse(br#"<div><p class="a">A</p><p class="b">B</p></div>"#);
let ua = user_agent_stylesheet();
let author =
parse_stylesheet(".a { height: 50px; margin: 0; } .b { height: 30px; margin: 0; }");
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut ps = Vec::new();
find_all(&dom, dom.document(), "p", &mut ps);
let a = find_laid_out(&laid, ps[0]).expect("p.a not found");
let b = find_laid_out(&laid, ps[1]).expect("p.b not found");
assert_eq!(
b.layout.content.y,
a.layout.content.y + a.layout.content.height
);
}
#[test]
fn equal_adjacent_margins_collapse_to_a_single_gap_instead_of_summing() {
let dom = html::parse(br#"<div><p class="a">A</p><p class="b">B</p></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
".a { height: 20px; margin: 16px 0; } .b { height: 20px; margin: 16px 0; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut ps = Vec::new();
find_all(&dom, dom.document(), "p", &mut ps);
let a = find_laid_out(&laid, ps[0]).expect("p.a not found");
let b = find_laid_out(&laid, ps[1]).expect("p.b not found");
let gap =
b.layout.border_box().y - (a.layout.border_box().y + a.layout.border_box().height);
assert_eq!(
gap, 16.0,
"equal adjacent margins should collapse to their shared value"
);
}
#[test]
fn left_float_is_removed_from_normal_flow_and_placed_at_containing_left() {
let dom = html::parse(
br#"<div class="outer"><div class="f">F</div><div class="after">after</div></div>"#,
);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
"body { margin: 0; } \
.f { float: left; width: 100px; height: 50px; } \
.after { height: 20px; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let float_box = find_laid_out(&laid, divs[1]).expect("float box not found");
let after_box = find_laid_out(&laid, divs[2]).expect("after box not found");
assert!(float_box.is_float);
assert_eq!(float_box.layout.content.x, 0.0);
assert_eq!(float_box.layout.content.y, 0.0);
assert_eq!(after_box.layout.content.y, 0.0);
}
#[test]
fn right_float_is_placed_against_the_containing_right_edge() {
let dom = html::parse(br#"<div class="outer"><div class="f">F</div></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
"body { margin: 0; } .f { float: right; width: 100px; height: 50px; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let float_box = find_laid_out(&laid, divs[1]).expect("float box not found");
assert_eq!(float_box.layout.content.x, 700.0);
assert_eq!(float_box.layout.content.y, 0.0);
}
#[test]
fn second_left_float_packs_next_to_the_first_instead_of_stacking() {
let dom = html::parse(
br#"<div class="outer"><div class="a">A</div><div class="b">B</div></div>"#,
);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
"body { margin: 0; } \
.a { float: left; width: 100px; height: 50px; } \
.b { float: left; width: 100px; height: 30px; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let a_box = find_laid_out(&laid, divs[1]).expect("a not found");
let b_box = find_laid_out(&laid, divs[2]).expect("b not found");
assert_eq!(a_box.layout.content.x, 0.0);
assert_eq!(b_box.layout.content.x, 100.0);
assert_eq!(b_box.layout.content.y, 0.0);
}
#[test]
fn floats_with_non_round_percentages_summing_to_100_sit_side_by_side() {
let dom = html::parse(
br#"<div class="outer"><div class="a">A</div><div class="b">B</div></div>"#,
);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
"body { margin: 0; } \
.a { float: left; width: 66.66667%; height: 50px; } \
.b { float: left; width: 33.33333%; height: 30px; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
for containing_width in [595.0, 612.0, 700.0, 793.7, 800.0, 1000.0] {
let laid = layout_document(&tree, &styles, &fonts, containing_width);
let a_box = find_laid_out(&laid, divs[1]).expect("a not found");
let b_box = find_laid_out(&laid, divs[2]).expect("b not found");
assert_eq!(
b_box.layout.content.y, 0.0,
"b stacked below a at containing width {containing_width}"
);
assert_eq!(b_box.layout.content.x, a_box.layout.content.width);
}
}
#[test]
fn clear_pushes_the_element_below_the_float() {
let dom = html::parse(
br#"<div class="outer"><div class="f">F</div><div class="c">after</div></div>"#,
);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
"body { margin: 0; } \
.f { float: left; width: 100px; height: 50px; } \
.c { clear: left; height: 20px; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let cleared_box = find_laid_out(&laid, divs[2]).expect("cleared box not found");
assert_eq!(cleared_box.layout.content.y, 50.0);
}
#[test]
fn float_does_not_participate_in_adjacent_margin_collapsing() {
let dom = html::parse(
br#"<div class="outer">
<div class="a">a</div><div class="f">F</div><div class="b">b</div>
</div>"#,
);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
"body { margin: 0; } \
.a { height: 10px; margin: 0 0 20px 0; } \
.f { float: left; width: 30px; height: 5px; } \
.b { height: 10px; margin: 30px 0 0 0; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let a_box = find_laid_out(&laid, divs[1]).expect("a not found");
let b_box = find_laid_out(&laid, divs[3]).expect("b not found");
assert_eq!(a_box.layout.content.y, 0.0);
assert_eq!(b_box.layout.content.y, 40.0);
}
#[test]
fn container_auto_height_expands_to_include_a_taller_float_child() {
let dom = html::parse(br#"<div class="outer"><div class="f">F</div></div>"#);
let ua = user_agent_stylesheet();
let author =
parse_stylesheet("body { margin: 0; } .f { float: left; width: 50px; height: 200px; }");
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let outer_box = find_laid_out(&laid, divs[0]).expect("outer not found");
assert_eq!(outer_box.layout.content.height, 200.0);
}
#[test]
fn position_relative_offsets_visual_position_without_affecting_siblings() {
let dom = html::parse(
br#"<div class="outer">
<div class="a">a</div><div class="rel">b</div><div class="c">c</div>
</div>"#,
);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
"body { margin: 0; } \
.a { height: 10px; } \
.rel { position: relative; top: 5px; left: 7px; height: 20px; } \
.c { height: 10px; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let rel_box = find_laid_out(&laid, divs[2]).expect("rel not found");
let c_box = find_laid_out(&laid, divs[3]).expect("c not found");
assert_eq!(rel_box.layout.content.x, 7.0);
assert_eq!(rel_box.layout.content.y, 15.0);
assert_eq!(c_box.layout.content.y, 30.0);
}
#[test]
fn unequal_adjacent_margins_collapse_to_the_larger_one() {
let dom = html::parse(br#"<div><p class="a">A</p><p class="b">B</p></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
".a { height: 20px; margin: 0 0 10px 0; } .b { height: 20px; margin: 24px 0 0 0; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut ps = Vec::new();
find_all(&dom, dom.document(), "p", &mut ps);
let a = find_laid_out(&laid, ps[0]).expect("p.a not found");
let b = find_laid_out(&laid, ps[1]).expect("p.b not found");
let gap =
b.layout.border_box().y - (a.layout.border_box().y + a.layout.border_box().height);
assert_eq!(
gap, 24.0,
"collapsed gap should be the larger of the two margins"
);
}
#[test]
fn a_negative_margin_reduces_the_collapsed_gap() {
let dom = html::parse(br#"<div><p class="a">A</p><p class="b">B</p></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
".a { height: 20px; margin: 0 0 10px 0; } .b { height: 20px; margin: -4px 0 0 0; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut ps = Vec::new();
find_all(&dom, dom.document(), "p", &mut ps);
let a = find_laid_out(&laid, ps[0]).expect("p.a not found");
let b = find_laid_out(&laid, ps[1]).expect("p.b not found");
let gap =
b.layout.border_box().y - (a.layout.border_box().y + a.layout.border_box().height);
assert_eq!(
gap, 6.0,
"positive + negative margins should sum (10 + (-4) = 6)"
);
}
#[test]
fn parent_and_first_child_top_margins_collapse_through_the_parent() {
let dom = html::parse(br#"<div class="outer"><p class="inner">x</p></div>"#);
let ua = user_agent_stylesheet();
let author =
parse_stylesheet(".outer { margin: 0; } .inner { height: 20px; margin: 12px 0; }");
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let outer = find_laid_out(&laid, divs[0]).expect("outer not found");
let mut ps = Vec::new();
find_all(&dom, dom.document(), "p", &mut ps);
let inner = find_laid_out(&laid, ps[0]).expect("inner not found");
assert_eq!(outer.layout.margin.top, 12.0);
assert_eq!(inner.layout.content.y, outer.layout.content.y);
assert_eq!(outer.layout.content.height, 20.0);
}
#[test]
fn a_top_border_on_the_parent_prevents_the_collapse() {
let dom = html::parse(br#"<div class="outer"><p class="inner">x</p></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
".outer { margin: 0; border-top: 5px solid black; } .inner { height: 20px; margin: 12px 0; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let outer = find_laid_out(&laid, divs[0]).expect("outer not found");
let mut ps = Vec::new();
find_all(&dom, dom.document(), "p", &mut ps);
let inner = find_laid_out(&laid, ps[0]).expect("inner not found");
assert_eq!(outer.layout.margin.top, 0.0, "no collapse through a border");
assert_eq!(inner.layout.content.y, outer.layout.content.y + 12.0);
}
#[test]
fn an_empty_block_collapses_its_own_top_and_bottom_margins() {
let dom = html::parse(br#"<div class="empty"></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(".empty { margin: 30px 0; }");
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let empty = find_laid_out(&laid, divs[0]).expect("empty not found");
assert_eq!(empty.layout.margin_box_height(), 30.0);
}
#[test]
fn auto_height_block_sizes_to_children_content() {
let dom = html::parse(br#"<div class="outer"><p class="inner">x</p></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(".inner { height: 40px; margin: 0; }");
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let outer = find_laid_out(&laid, divs[0]).expect("outer div not found");
assert_eq!(outer.layout.content.height, 40.0);
}
#[test]
fn wrapped_inline_content_drives_auto_height() {
let dom = html::parse(br#"<p class="a">hello world</p>"#);
let ua = user_agent_stylesheet();
let author = Stylesheet::default();
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let mut ps = Vec::new();
find_all(&dom, dom.document(), "p", &mut ps);
let wide = layout_document(&tree, &styles, &fonts, 800.0);
let p_wide = find_laid_out(&wide, ps[0]).expect("p not found");
let LaidOutContent::Inline(lines_wide) = &p_wide.content else {
panic!("expected inline content")
};
assert_eq!(lines_wide.len(), 1);
let narrow = layout_document(&tree, &styles, &fonts, 60.0);
let p_narrow = find_laid_out(&narrow, ps[0]).expect("p not found");
let LaidOutContent::Inline(lines_narrow) = &p_narrow.content else {
panic!("expected inline content")
};
assert_eq!(lines_narrow.len(), 2);
assert!(p_narrow.layout.content.height > p_wide.layout.content.height);
}
#[test]
fn padding_and_border_offset_content_box() {
let dom = html::parse(br#"<div class="box"></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
".box { width: 100px; margin: 0; padding: 5px; border: 2px solid black; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let div_box = find_laid_out(&laid, divs[0]).expect("div box not found");
assert_eq!(div_box.layout.content.width, 100.0);
assert_eq!(div_box.layout.padding.left, 5.0);
assert_eq!(div_box.layout.border.left, 2.0);
let border_box = div_box.layout.border_box();
assert_eq!(border_box.width, 2.0 + 5.0 + 100.0 + 5.0 + 2.0);
}
#[test]
fn box_sizing_border_box_makes_the_specified_width_include_padding_and_border() {
let dom = html::parse(br#"<div class="box"></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
".box { box-sizing: border-box; width: 100px; height: 60px; margin: 0; \
padding: 5px; border: 2px solid black; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let div_box = find_laid_out(&laid, divs[0]).expect("div box not found");
assert_eq!(div_box.layout.content.width, 100.0 - 2.0 * 5.0 - 2.0 * 2.0);
assert_eq!(div_box.layout.content.height, 60.0 - 2.0 * 5.0 - 2.0 * 2.0);
let border_box = div_box.layout.border_box();
assert_eq!(border_box.width, 100.0);
assert_eq!(border_box.height, 60.0);
}
#[test]
fn box_sizing_border_box_clamps_to_zero_when_padding_and_border_exceed_the_specified_width() {
let dom = html::parse(br#"<div class="box"></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
".box { box-sizing: border-box; width: 5px; margin: 0; \
padding: 10px; border: 10px solid black; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let div_box = find_laid_out(&laid, divs[0]).expect("div box not found");
assert_eq!(div_box.layout.content.width, 0.0);
}
#[test]
fn border_style_none_zeroes_out_the_used_border_width_in_layout() {
let dom = html::parse(br#"<div class="box"></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
".box { width: 100px; margin: 0; border-width: 5px; border-style: none; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let div_box = find_laid_out(&laid, divs[0]).expect("div box not found");
assert_eq!(div_box.layout.border.left, 0.0);
let border_box = div_box.layout.border_box();
assert_eq!(border_box.width, 100.0);
}
#[test]
fn fragmentation_hints_reflect_the_elements_computed_style() {
let dom = html::parse(br#"<div class="box"></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
".box { break-before: always; break-inside: avoid; orphans: 3; widows: 4; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let div_box = find_laid_out(&laid, divs[0]).expect("div box not found");
assert_eq!(
div_box.fragmentation.break_before,
super::BreakBetween::Always
);
assert_eq!(div_box.fragmentation.break_after, super::BreakBetween::Auto);
assert_eq!(
div_box.fragmentation.break_inside,
super::BreakInside::Avoid
);
assert_eq!(div_box.fragmentation.orphans, 3);
assert_eq!(div_box.fragmentation.widows, 4);
}
#[test]
fn anonymous_boxes_get_default_fragmentation_hints() {
let dom = html::parse(br#"<div class="outer">before <p>P</p> after</div>"#);
let ua = user_agent_stylesheet();
let author = Stylesheet::default();
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut divs = Vec::new();
find_all(&dom, dom.document(), "div", &mut divs);
let div_box = find_laid_out(&laid, divs[0]).expect("div box not found");
let LaidOutContent::Blocks(children) = &div_box.content else {
panic!("expected block container")
};
let anonymous = children
.iter()
.find(|c| c.node.is_none())
.expect("expected an anonymous block wrapping the loose text");
assert_eq!(anonymous.fragmentation, FragmentationHints::default());
}
fn image_prepared(width: f32, height: f32) -> Rc<PreparedImage> {
Rc::new(PreparedImage {
width,
height,
content: crate::pdf::PreparedContent::Raster {
color: ImagePlane {
data: Vec::new(),
filter: pdf_writer::Filter::FlateDecode,
color_space: PlaneColorSpace::Rgb,
bits_per_component: 8,
},
alpha: None,
},
})
}
fn image_box(content: ImageBoxContent) -> LayoutBox {
LayoutBox::anonymous(BoxContent::Image(content))
}
#[test]
fn image_with_no_attrs_uses_intrinsic_size_when_decoded() {
let tree = image_box(ImageBoxContent {
image: Some(image_prepared(200.0, 100.0)),
attr_width: None,
attr_height: None,
});
let laid = layout_document(&tree, &HashMap::new(), &test_fonts(), 800.0);
assert_eq!(laid.layout.content.width, 200.0);
assert_eq!(laid.layout.content.height, 100.0);
}
#[test]
fn image_width_attr_only_derives_height_via_aspect_ratio() {
let tree = image_box(ImageBoxContent {
image: Some(image_prepared(200.0, 100.0)),
attr_width: Some(50),
attr_height: None,
});
let laid = layout_document(&tree, &HashMap::new(), &test_fonts(), 800.0);
assert_eq!(laid.layout.content.width, 50.0);
assert_eq!(laid.layout.content.height, 25.0);
}
#[test]
fn image_height_attr_only_derives_width_via_aspect_ratio() {
let tree = image_box(ImageBoxContent {
image: Some(image_prepared(200.0, 100.0)),
attr_width: None,
attr_height: Some(40),
});
let laid = layout_document(&tree, &HashMap::new(), &test_fonts(), 800.0);
assert_eq!(laid.layout.content.height, 40.0);
assert_eq!(laid.layout.content.width, 80.0);
}
#[test]
fn image_with_both_attrs_ignores_the_intrinsic_aspect_ratio() {
let tree = image_box(ImageBoxContent {
image: Some(image_prepared(200.0, 100.0)),
attr_width: Some(10),
attr_height: Some(10),
});
let laid = layout_document(&tree, &HashMap::new(), &test_fonts(), 800.0);
assert_eq!(laid.layout.content.width, 10.0);
assert_eq!(laid.layout.content.height, 10.0);
}
#[test]
fn failed_image_with_no_attrs_collapses_to_zero_size() {
let tree = image_box(ImageBoxContent {
image: None,
attr_width: None,
attr_height: None,
});
let laid = layout_document(&tree, &HashMap::new(), &test_fonts(), 800.0);
assert_eq!(laid.layout.content.width, 0.0);
assert_eq!(laid.layout.content.height, 0.0);
}
#[test]
fn failed_image_with_explicit_attrs_still_reserves_the_specified_space() {
let tree = image_box(ImageBoxContent {
image: None,
attr_width: Some(50),
attr_height: Some(50),
});
let laid = layout_document(&tree, &HashMap::new(), &test_fonts(), 800.0);
assert_eq!(laid.layout.content.width, 50.0);
assert_eq!(laid.layout.content.height, 50.0);
}
#[test]
fn image_does_not_stretch_to_fill_the_containing_block_like_a_block_div_would() {
let tree = image_box(ImageBoxContent {
image: Some(image_prepared(50.0, 50.0)),
attr_width: None,
attr_height: None,
});
let laid = layout_document(&tree, &HashMap::new(), &test_fonts(), 800.0);
assert_eq!(laid.layout.content.width, 50.0);
}
#[test]
fn outside_marker_is_positioned_left_of_the_content_edge_with_a_fixed_gap() {
let dom = html::parse(br#"<ul><li>text</li></ul>"#);
let ua = user_agent_stylesheet();
let styles = compute_styles(&dom, &ua, &Stylesheet::default());
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let mut lis = Vec::new();
find_all(&dom, dom.document(), "li", &mut lis);
let li = find_laid_out(&laid, lis[0]).expect("li not found");
let marker = li.marker.as_ref().expect("li should have a marker");
assert_eq!(marker.runs.len(), 1);
assert!(marker.rect.width > 0.0);
assert_eq!(
marker.rect.x,
li.layout.content.x - LIST_MARKER_GAP - marker.rect.width
);
assert_eq!(
marker.rect.y, li.layout.content.y,
"marker should align with the top of the li's own content"
);
}
#[test]
fn list_style_type_none_produces_no_marker_in_the_laid_out_box() {
let dom = html::parse(br#"<ul><li style="list-style-type: none;">text</li></ul>"#);
let ua = user_agent_stylesheet();
let styles = compute_styles(&dom, &ua, &Stylesheet::default());
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let li = find(&dom, dom.document(), "li").expect("li not found");
let li_laid = find_laid_out(&laid, li).expect("li not found");
assert!(li_laid.marker.is_none());
}
fn find(dom: &Dom, id: NodeId, tag: &str) -> Option<NodeId> {
if let NodeData::Element { name, .. } = &dom.node(id).data {
if &*name.local == tag {
return Some(id);
}
}
dom.children(id).find_map(|child| find(dom, child, tag))
}
#[test]
fn position_relative_moves_inline_content_and_block_children_with_the_box() {
let dom = html::parse(
br#"<div class="rel"><p>child</p></div><ul><li class="rel">item</li></ul>"#,
);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
"body, p, ul { margin: 0; padding: 0; } \
.rel { position: relative; top: 5px; left: 7px; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let div = find(&dom, dom.document(), "div").expect("div not found");
let p = find(&dom, dom.document(), "p").expect("p not found");
let div_box = find_laid_out(&laid, div).expect("div not laid out");
let p_box = find_laid_out(&laid, p).expect("p not laid out");
assert_eq!(
(div_box.layout.content.x, div_box.layout.content.y),
(7.0, 5.0)
);
assert_eq!((p_box.layout.content.x, p_box.layout.content.y), (7.0, 5.0));
let LaidOutContent::Inline(lines) = &p_box.content else {
panic!("p should hold inline content");
};
assert_eq!((lines[0].rect.x, lines[0].rect.y), (7.0, 5.0));
let li = find(&dom, dom.document(), "li").expect("li not found");
let li_box = find_laid_out(&laid, li).expect("li not laid out");
let marker = li_box.marker.as_ref().expect("li should have a marker");
let LaidOutContent::Inline(lines) = &li_box.content else {
panic!("li should hold inline content");
};
assert_eq!(lines[0].rect.x, li_box.layout.content.x);
assert!(marker.rect.x < li_box.layout.content.x);
assert_eq!(marker.rect.y, lines[0].rect.y);
}
#[test]
fn position_relative_offset_shifts_the_containing_block_of_absolute_children() {
let dom = html::parse(br#"<div class="rel"><div class="abs">x</div></div>"#);
let ua = user_agent_stylesheet();
let author = parse_stylesheet(
"body { margin: 0; } \
.rel { position: relative; top: 5px; left: 7px; height: 50px; } \
.abs { position: absolute; top: 0; left: 0; }",
);
let styles = compute_styles(&dom, &ua, &author);
let tree = build_box_tree(&dom, &styles);
let fonts = test_fonts();
let (_, positioned) = layout_document_positioned(&tree, &styles, &fonts, (800.0, 600.0));
assert_eq!(positioned.len(), 1);
let abs = &positioned[0].laid;
assert_eq!((abs.layout.content.x, abs.layout.content.y), (7.0, 5.0));
}
#[test]
fn position_relative_on_an_inline_element_shifts_only_its_runs() {
let html_src = br#"<div>A <span class="s">TEXT</span> B</div>"#;
let ua = user_agent_stylesheet();
let fonts = test_fonts();
let runs_of = |css: &str| -> Vec<(String, f32, f32)> {
let dom = html::parse(html_src);
let styles = compute_styles(&dom, &ua, &parse_stylesheet(css));
let tree = build_box_tree(&dom, &styles);
let laid = layout_document(&tree, &styles, &fonts, 800.0);
let div = find(&dom, dom.document(), "div").expect("div not found");
let div_box = find_laid_out(&laid, div).expect("div not laid out");
let LaidOutContent::Inline(lines) = &div_box.content else {
panic!("div should hold inline content");
};
lines[0]
.runs
.iter()
.map(|r| (r.text.clone(), r.x_offset, r.baseline_shift))
.collect()
};
let plain = runs_of("body { margin: 0; }");
let shifted =
runs_of("body { margin: 0; } .s { position: relative; left: 120px; top: 3px; }");
assert_eq!(plain.len(), shifted.len());
for (before, after) in plain.iter().zip(&shifted) {
assert_eq!(before.0, after.0);
if before.0 == "TEXT" {
assert_eq!(
after.1,
before.1 + 120.0,
"span run should move right by 120px"
);
assert_eq!(after.2, before.2 - 3.0, "span run should move down by 3px");
} else {
assert_eq!(
(after.1, after.2),
(before.1, before.2),
"{:?} should not move",
before.0
);
}
}
}
}