跳转至

布局引擎 API

布局模块包含 CSS Grid 求解器,负责轨道尺寸计算、子项放置和对齐。

模块概览

职责
GridSolver Grid 布局求解器主类
TrackDef 轨道定义(fixed/fr/auto/minmax/etc.)
Track 求解后的轨道(含实际尺寸)
GridItem 包装子节点的 Grid 项
SizeType 尺寸类型枚举

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))

自动生成的 API 文档

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)