use std::collections::BTreeMap;
use dom_cat::{Document, Node, NodeId};
use crate::box_tree::{LayoutBox, LayoutTree, Viewport};
use crate::length::{DEFAULT_FONT_SIZE_PX, Edges, LengthValue, Point, Rect};
use crate::style::{ComputedStyle, Display};
#[must_use]
pub fn layout_document(
doc: &Document,
styles: &BTreeMap<NodeId, ComputedStyle>,
viewport: Viewport,
) -> LayoutTree {
let body = find_body(doc).unwrap_or(doc.root());
let root = layout_block(doc, styles, body, viewport_width(viewport), 0.0, 0.0);
LayoutTree::new(root, viewport)
}
fn viewport_width(v: Viewport) -> f64 {
f64::from(v.width())
}
fn find_body(doc: &Document) -> Option<NodeId> {
doc.get(doc.root())
.map_or(&[][..], Node::children)
.iter()
.find_map(|&child_id| {
doc.get(child_id)
.and_then(Node::as_element)
.filter(|e| e.name() == "html")
.map(|_| child_id)
})
.and_then(|html_id| {
doc.get(html_id)
.map_or(&[][..], Node::children)
.iter()
.find_map(|&child_id| {
doc.get(child_id)
.and_then(Node::as_element)
.filter(|e| e.name() == "body")
.map(|_| child_id)
})
})
}
fn layout_block(
doc: &Document,
styles: &BTreeMap<NodeId, ComputedStyle>,
id: NodeId,
containing_width: f64,
x: f64,
y: f64,
) -> Option<LayoutBox> {
let style = styles
.get(&id)
.cloned()
.unwrap_or_else(ComputedStyle::default);
if matches!(style.display(), Display::None) {
None
} else {
Some(layout_block_box(
doc,
styles,
id,
style,
containing_width,
x,
y,
))
}
}
fn layout_block_box(
doc: &Document,
styles: &BTreeMap<NodeId, ComputedStyle>,
id: NodeId,
style: ComputedStyle,
containing_width: f64,
x: f64,
y: f64,
) -> LayoutBox {
let font_size = style
.font_size()
.resolve(containing_width, DEFAULT_FONT_SIZE_PX)
.unwrap_or(DEFAULT_FONT_SIZE_PX);
let margin = resolve_edges(style.margins(), containing_width, font_size);
let padding = resolve_edges(style.padding(), containing_width, font_size);
let border = resolve_edges(style.border_widths(), containing_width, font_size);
let total_horizontal_extra = margin.horizontal() + padding.horizontal() + border.horizontal();
let content_width = style
.width()
.resolve(containing_width, font_size)
.unwrap_or_else(|| (containing_width - total_horizontal_extra).max(0.0));
let content_x = x + margin.left() + border.left() + padding.left();
let content_y_start = y + margin.top() + border.top() + padding.top();
let element_children = element_children(doc, id);
let (laid_out_children, content_height) = layout_children(
doc,
styles,
&element_children,
content_width,
content_x,
content_y_start,
);
let explicit_height = style.height().resolve(containing_width, font_size);
let final_content_height = explicit_height.unwrap_or(content_height);
let content_rect = Rect::new(
Point::new(content_x, content_y_start),
content_width,
final_content_height,
);
LayoutBox::new(
id,
style,
content_rect,
margin,
border,
padding,
laid_out_children,
)
}
fn layout_children(
doc: &Document,
styles: &BTreeMap<NodeId, ComputedStyle>,
children: &[NodeId],
containing_width: f64,
x: f64,
start_y: f64,
) -> (Vec<LayoutBox>, f64) {
let (boxes, end_y) = children.iter().fold(
(Vec::<LayoutBox>::new(), start_y),
|(acc, cursor_y), &child_id| {
layout_block(doc, styles, child_id, containing_width, x, cursor_y).map_or(
(acc.clone(), cursor_y),
|laid_out| {
let consumed = consumed_height(&laid_out);
let new_y = cursor_y + consumed;
let extended: Vec<LayoutBox> =
acc.into_iter().chain(std::iter::once(laid_out)).collect();
(extended, new_y)
},
)
},
);
(boxes, end_y - start_y)
}
fn consumed_height(child: &LayoutBox) -> f64 {
let m = child.margin();
let b = child.border();
let p = child.padding();
let h = child.rect().height();
m.top() + b.top() + p.top() + h + p.bottom() + b.bottom() + m.bottom()
}
fn element_children(doc: &Document, parent: NodeId) -> Vec<NodeId> {
doc.get(parent)
.map_or(&[][..], Node::children)
.iter()
.copied()
.filter(|id| doc.get(*id).is_some_and(|n| matches!(n, Node::Element(_))))
.collect()
}
fn resolve_edges(
edges: (LengthValue, LengthValue, LengthValue, LengthValue),
containing_width: f64,
font_size: f64,
) -> Edges {
let (top, right, bottom, left) = edges;
Edges::new(
top.resolve_or_zero(containing_width, font_size),
right.resolve_or_zero(containing_width, font_size),
bottom.resolve_or_zero(containing_width, font_size),
left.resolve_or_zero(containing_width, font_size),
)
}