Skip to content

Layout Engine API

The layout module contains the CSS Grid solver, responsible for track sizing, item placement, and alignment.

Module Overview

Class Responsibility
GridSolver Main grid layout solver
TrackDef Track definition (fixed/fr/auto/minmax/etc.)
Track Solved track (with actual size)
GridItem Grid item wrapping child nodes
SizeType Size type enum

GridSolver

from latticesvg.layout.grid_solver import GridSolver
from latticesvg import GridContainer, LayoutConstraints

grid = GridContainer(style={"grid-template-columns": "1fr 2fr", "width": 600})
solver = GridSolver(grid)
solver.solve(LayoutConstraints(available_width=600))

Auto-generated API Docs

grid_solver

CSS Grid Level 1 layout solver.

Implements track sizing, item placement, coordinate computation, and alignment — the heart of the LatticeSVG layout engine.

TrackDef dataclass

TrackDef(size_type: SizeType, value: float = 0.0, min_track: Optional['TrackDef'] = None, max_track: Optional['TrackDef'] = None)

Parsed definition for one grid track (column or row).

Track dataclass

Track(definition: TrackDef, base_size: float = 0.0, growth_limit: float = float('inf'), final_size: float = 0.0)

Runtime state for a single track during sizing.

GridItem dataclass

GridItem(node: 'Node', row_start: int, row_end: int, col_start: int, col_end: int)

A child node with its resolved grid placement.

GridSolver

GridSolver(container: 'GridContainer')

Performs CSS Grid layout on a :class:GridContainer.

Source code in src/latticesvg/layout/grid_solver.py
def __init__(self, container: "GridContainer") -> None:
    self.container = container
    self.col_tracks: List[Track] = []
    self.row_tracks: List[Track] = []
    self.items: List[GridItem] = []
    self._col_gap: float = 0.0
    self._row_gap: float = 0.0
solve
solve(constraints: 'LayoutConstraints') -> None

Run the complete grid layout algorithm.

Source code in src/latticesvg/layout/grid_solver.py
def solve(self, constraints: "LayoutConstraints") -> None:
    """Run the complete grid layout algorithm."""
    from ..nodes.base import LayoutConstraints as LC, Rect

    s = self.container.style

    # ---- Resolve container width --------------------------------
    container_w = self.container._resolve_width(constraints)
    if container_w is None:
        container_w = constraints.available_width or 800.0
        # available_width is always a border-box concept
        content_w = max(
            0.0,
            container_w - s.padding_horizontal - s.border_horizontal,
        )
    else:
        # _resolve_width returns the raw value; convert via box-sizing
        content_w = self.container._width_to_content(container_w)

    # ---- Gap ----------------------------------------------------
    self._col_gap = s._float("column-gap")
    self._row_gap = s._float("row-gap")

    # ---- Parse track templates ----------------------------------
    col_defs = self._parse_track_defs(s.get("grid-template-columns"))
    row_defs = self._parse_track_defs(s.get("grid-template-rows"))

    # ---- Parse grid-template-areas & ensure track counts ---------
    area_mapping = s.get("grid-template-areas")
    if isinstance(area_mapping, AreaMapping):
        while len(col_defs) < area_mapping.num_cols:
            col_defs.append(self._implicit_track_def("col"))
        while len(row_defs) < area_mapping.num_rows:
            row_defs.append(self._implicit_track_def("row"))

    # ---- Place items --------------------------------------------
    self.items = self._place_items(col_defs, row_defs, area_mapping)

    # Ensure enough tracks exist for all items
    max_col = max((it.col_end for it in self.items), default=len(col_defs))
    max_row = max((it.row_end for it in self.items), default=len(row_defs))
    while len(col_defs) < max_col:
        col_defs.append(self._implicit_track_def("col"))
    while len(row_defs) < max_row:
        row_defs.append(self._implicit_track_def("row"))

    self.col_tracks = [Track(d) for d in col_defs]
    self.row_tracks = [Track(d) for d in row_defs]

    # ---- Column sizing ------------------------------------------
    self._resolve_tracks_axis(
        self.col_tracks,
        content_w,
        self._col_gap,
        axis="col",
    )

    # ---- Row sizing (depends on column widths for text reflow) --
    # First compute item content heights given column widths
    raw_h = self.container._resolve_height(constraints)
    content_h_for_rows = (
        self.container._height_to_content(raw_h) if raw_h is not None else None
    )
    self._resolve_tracks_axis(
        self.row_tracks,
        content_h_for_rows,
        self._row_gap,
        axis="row",
    )

    total_row_height = (
        sum(t.final_size for t in self.row_tracks)
        + self._row_gap * max(0, len(self.row_tracks) - 1)
    )

    # ---- Container height ---------------------------------------
    container_h = self.container._resolve_height(constraints)
    if container_h is None:
        content_h = total_row_height
    else:
        content_h = self.container._height_to_content(container_h)

    # ---- Resolve container boxes --------------------------------
    self.container._resolve_box_model(content_w, content_h)

    # ---- Compute child positions --------------------------------
    self._compute_positions()

    # ---- Layout children recursively ----------------------------
    for item in self.items:
        cell_w = self._item_column_width(item)
        cell_h = self._item_row_height(item)

        # Determine alignment — non-stretch items should layout at
        # their intrinsic size, not the full cell size.
        ns = item.node.style
        cs = self.container.style
        justify = ns.get("justify-self")
        if justify is None or justify == "auto" or isinstance(justify, AutoValue):
            justify = cs.get("justify-items") or "stretch"
        align = ns.get("align-self")
        if align is None or align == "auto" or isinstance(align, AutoValue):
            align = cs.get("align-items") or "stretch"

        if justify != "stretch":
            # Use the child's intrinsic width
            c_meas = LC(available_width=cell_w)
            _, max_w, _ = item.node.measure(c_meas)
            layout_w = min(max_w, cell_w)
        else:
            layout_w = cell_w

        if align != "stretch":
            c_meas = LC(available_width=layout_w)
            _, _, intr_h = item.node.measure(c_meas)
            layout_h = min(intr_h, cell_h)
        else:
            layout_h = cell_h

        child_constraints = LC(
            available_width=layout_w,
            available_height=layout_h,
        )
        item.node.layout(child_constraints)

        # Apply alignment to position within the cell
        self._apply_alignment(item)

        # Apply min/max width/height constraints (after alignment,
        # so that stretch doesn't override the clamp).
        self._clamp_min_max(item.node)
measure
measure(constraints: 'LayoutConstraints') -> Tuple[float, float, float]

Measure the grid container's intrinsic sizes.

Source code in src/latticesvg/layout/grid_solver.py
def measure(self, constraints: "LayoutConstraints") -> Tuple[float, float, float]:
    """Measure the grid container's intrinsic sizes."""
    s = self.container.style
    col_defs = self._parse_track_defs(s.get("grid-template-columns"))
    row_defs = self._parse_track_defs(s.get("grid-template-rows"))

    self._col_gap = s._float("column-gap")
    self._row_gap = s._float("row-gap")

    # ---- Parse grid-template-areas & ensure track counts ---------
    area_mapping = s.get("grid-template-areas")
    if isinstance(area_mapping, AreaMapping):
        while len(col_defs) < area_mapping.num_cols:
            col_defs.append(self._implicit_track_def("col"))
        while len(row_defs) < area_mapping.num_rows:
            row_defs.append(self._implicit_track_def("row"))

    self.items = self._place_items(col_defs, row_defs, area_mapping)

    max_col = max((it.col_end for it in self.items), default=len(col_defs))
    max_row = max((it.row_end for it in self.items), default=len(row_defs))
    while len(col_defs) < max_col:
        col_defs.append(self._implicit_track_def("col"))
    while len(row_defs) < max_row:
        row_defs.append(self._implicit_track_def("row"))

    self.col_tracks = [Track(d) for d in col_defs]
    self.row_tracks = [Track(d) for d in row_defs]

    # Min-content: resolve columns at min sizes
    self._resolve_tracks_min_content(self.col_tracks, self._col_gap, axis="col")
    min_w = (
        sum(t.final_size for t in self.col_tracks)
        + self._col_gap * max(0, len(self.col_tracks) - 1)
    )

    # Max-content: resolve columns at max sizes
    self._resolve_tracks_max_content(self.col_tracks, self._col_gap, axis="col")
    max_w = (
        sum(t.final_size for t in self.col_tracks)
        + self._col_gap * max(0, len(self.col_tracks) - 1)
    )

    # Height at max-content width
    self._resolve_tracks_axis(self.row_tracks, None, self._row_gap, axis="row")
    h = (
        sum(t.final_size for t in self.row_tracks)
        + self._row_gap * max(0, len(self.row_tracks) - 1)
    )

    ph = s.padding_horizontal + s.border_horizontal
    pv = s.padding_vertical + s.border_vertical
    return (min_w + ph, max_w + ph, h + pv)