layout-cat 0.1.0

Box-model layout: cascade + block layout over a dom-cat tree using css-cat stylesheets. Produces a LayoutBox tree with positions and dimensions. No mut, no Rc/Arc, no interior mutability, no panics, exhaustive matches. Fourth sub-crate of a Servo-replacement webview runtime targeting Tauri.
//! Block layout: vertical stacking of children with width/height
//! arithmetic.

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};

/// Lay out the document.
#[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),
    )
}