Skip to content

Step 5: Block and inline layout #24

@thomasnemer

Description

@thomasnemer

Parent: #19

Goal

Implement the core layout engine: given a styled DOM tree and viewport dimensions, produce a LayoutTree with precise geometry (position and size) for every box. Covers block formatting context, inline formatting context, and CSS positioned layout.

Prerequisites

  • Step 4 (style resolution — computed styles for every node)

File Changes

  • crates/ie-layout/src/lib.rs — restructure, expand LayoutTree/LayoutBox types
  • crates/ie-layout/src/box_generation.rs — new file
  • crates/ie-layout/src/block.rs — new file
  • crates/ie-layout/src/inline.rs — new file
  • crates/ie-layout/src/positioned.rs — new file
  • crates/ie-layout/src/text_measure.rs — new file, TextMeasure trait

Implementation

Expanded types (lib.rs)

  • LayoutBox:
    pub struct LayoutBox {
        pub node_id: Option<NodeId>,    // None for anonymous boxes
        pub box_type: BoxType,
        pub style: ComputedStyle,
        pub content_rect: Rect,
        pub padding: EdgeSizes,
        pub border: EdgeSizes,
        pub margin: EdgeSizes,
        pub children: Vec<usize>,       // indices into LayoutTree arena
    }
    pub enum BoxType { Block, Inline, InlineBlock, Anonymous, Text(String) }
    pub struct EdgeSizes { pub top: f32, pub right: f32, pub bottom: f32, pub left: f32 }
  • LayoutTree: arena of LayoutBox (Vec<LayoutBox> indexed by usize)
  • layout(doc: &Document, styles: &[ComputedStyle], viewport: Rect, text_measure: &dyn TextMeasure) -> LayoutTree

TextMeasure trait (text_measure.rs)

  • Defined in ie-layout, implemented by ie-render (trait inversion):
    pub trait TextMeasure {
        fn measure(&self, text: &str, style: &ComputedStyle) -> TextMetrics;
    }
    pub struct TextMetrics {
        pub width: f32,
        pub height: f32,
        pub ascent: f32,
        pub descent: f32,
    }
  • For testing: MockTextMeasure that returns width = text.len() * 8.0, height = style.font_size

Box generation (box_generation.rs)

  • Walk styled DOM, generate box tree:
    • display: none → skip (no box)
    • display: block → BlockBox
    • display: inline → InlineBox
    • display: inline-block → InlineBlockBox
    • display: flex → FlexBox (handled in Step 7, generate as Block for now)
    • Text nodes → TextBox
  • Anonymous box insertion:
    • Block container with mixed block+inline children → wrap inline sequences in anonymous block box
    • Inline container with block child → split inline context
  • Resolve padding, border, margin from ComputedStyle into EdgeSizes

Block formatting context (block.rs)

  • fn layout_block(box_idx: usize, tree: &mut LayoutTree, containing_width: f32, text_measure: &dyn TextMeasure):
    1. Compute width: CSS2 width algorithm:
      • If width is auto: width = containing_width - margin_left - margin_right - padding_left - padding_right - border_left - border_right
      • If width is specified: use it
      • If margin-left and margin-right are both auto: center the box (split remaining space)
      • Handle min-width / max-width clamping
    2. Layout children: position each child block below the previous one (y = previous_bottom)
    3. Compute height: if height is auto: sum of children heights. If specified: use it. Handle min-height / max-height.
    4. Margin collapsing:
      • Adjacent sibling margins collapse (use the larger of the two)
      • Parent-child margin collapsing (first/last child margin merges with parent)
      • Empty blocks collapse top and bottom margins
      • Negative margin handling
    5. box-sizing: content-box (default) vs border-box (include padding+border in width/height)

Inline formatting context (inline.rs)

  • fn layout_inline(box_idx: usize, tree: &mut LayoutTree, containing_width: f32, text_measure: &dyn TextMeasure):
    1. Line box construction:
      • Accumulate inline boxes on a line
      • Measure text content via text_measure.measure()
      • When accumulated width exceeds containing_width, break to next line
    2. Line breaking:
      • Break at whitespace boundaries (greedy: fill as much as possible on each line)
      • white-space: normal → collapse whitespace, allow wrapping
      • white-space: nowrap → collapse whitespace, no wrapping
      • white-space: pre → preserve whitespace, no wrapping
      • white-space: pre-wrap → preserve whitespace, allow wrapping
    3. Baseline alignment: align inline boxes on the baseline within each line box
    4. text-align: left (default), center, right (justify deferred)
    5. Inline box splitting: if an inline box wraps to multiple lines, it's split into fragments (one per line)

Positioned layout (positioned.rs)

  • position: static — default, normal flow
  • position: relative — lay out in normal flow, then offset by top/left/right/bottom
  • position: absolute:
    • Remove from normal flow (doesn't affect siblings)
    • Position relative to nearest positioned ancestor (walk up the tree to find position != static)
    • Resolve top/left/right/bottom against that ancestor's padding box
  • position: fixed — like absolute but relative to viewport
  • Stacking context: track z-index for paint ordering (consumed by ie-render in Step 6)
    • Elements with position != static && z-index != auto create stacking contexts
    • opacity < 1 also creates a stacking context

Tests

All tests use MockTextMeasure (8px per character, height = font-size).

Block layout tests

  • Single div, viewport 800px: content_rect.width = 800 - margins/padding/borders
  • Nested divs: inner width = outer content width - inner margins/padding/borders
  • Explicit width: width: 200px → content_rect.width = 200
  • Auto margins centering: width: 200px; margin: 0 auto → centered in 800px viewport
  • Height auto: parent height = sum of children
  • Margin collapsing: two adjacent divs with margin-bottom: 20px and margin-top: 30px → gap is 30px (not 50px)
  • box-sizing: border-box: width includes padding+border
  • min-width / max-width clamping

Inline layout tests

  • Short text fits on one line: width = text length * 8px
  • Long text wraps: 100-char text in 400px container → wraps at ~50 chars
  • white-space: nowrap → no wrapping regardless of container width
  • text-align: center → line content centered
  • Multiple inline elements on same line: positioned left-to-right
  • Mixed block and inline: inline sequences wrapped in anonymous block

Positioned layout tests

  • position: relative; top: 10px; left: 20px → offset from normal position
  • position: absolute → removed from flow, siblings not affected
  • position: absolute with nearest positioned ancestor → positioned relative to that ancestor
  • position: fixed → positioned relative to viewport rect

Acceptance Criteria

  • cargo test -p ie-layout — all tests pass
  • Layout produces correct geometry for block + inline content
  • Margin collapsing is correct
  • Text wrapping works with mock text measurer
  • Positioned elements placed correctly
  • cargo clippy -p ie-layout -- -D warnings — no warnings

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions