Skip to content

Layout

The pure-Python flexbox engine that computes a frame (x, y, width, height) for every node in the rendered tree. The Reconciler runs calculate_layout after every commit and forwards the resulting frames to the platform handlers via set_frame.

For a conceptual overview, see Layout engine.

Pure-Python flexbox layout engine.

Computes positions and sizes for every node in a layout tree based on CSS flexbox-inspired style properties. Inspired by Facebook's Yoga and React Native's layout system, but implemented entirely in Python so PythonNative does not depend on a native layout library.

The engine is invoked by the reconciler after each commit pass:

  1. The reconciler maintains a parallel LayoutNode tree (cached across passes: clean subtrees keep their nodes, dirty ones are rebuilt).
  2. calculate_layout is called with the viewport size; it recursively determines each node's (x, y, width, height) relative to its parent's coordinate space. Nodes that are not dirty and are measured under the same constraints as the previous pass return their memoized size without recursing, which turns full-tree layout into dirty-subtree layout.
  3. The reconciler walks the tree again and emits a frame op for each native view whose frame actually changed.

The algorithm supports:

  • Flex containers: flex_direction (row/column and their reverse variants), justify_content (flex_start / center / flex_end / space_between / space_around / space_evenly), align_items (stretch / flex_start / center / flex_end), and align_self overrides per child.
  • Wrapping: flex_wrap (nowrap / wrap / wrap_reverse) with align_content controlling how lines share leftover cross-axis space (stretch default, plus the justify palette).
  • Direction: direction: "rtl" flips row layouts, start / end edge keys (margin_start, padding_end, absolute start / end insets) resolve against the inherited direction.
  • Sizing: explicit width / height (numbers or percentages), min_width / max_width / min_height / max_height constraints, aspect_ratio, and content-based sizing via the optional measure callback.
  • Flex distribution: flex (RN shorthand for grow factor with flex_basis: 0), flex_grow, flex_shrink, flex_basis.
  • Absolute positioning: position: "absolute" with top, right, bottom, left (and start / end) insets. Absolute children are positioned relative to the parent's padding box and do not participate in flex distribution.
  • Spacing: padding / margin (scalar, dict, or per-edge keys), inter-child spacing (aliases: gap, column_gap / row_gap per axis).
Example
from pythonnative.layout import LayoutNode, calculate_layout

root = LayoutNode(
    style={"flex_direction": "row", "padding": 8, "spacing": 4},
    children=[
        LayoutNode(style={"width": 80, "height": 40}),
        LayoutNode(style={"flex": 1, "height": 40}),
        LayoutNode(style={"width": 60, "height": 40}),
    ],
)
calculate_layout(root, 320, 200)
# root.children[0].x == 8, .width == 80
# root.children[1].x == 92, .width == 156  (filled by flex: 1)
# root.children[2].x == 252, .width == 60

Classes:

Name Description
LayoutNode

A node in the layout tree.

Functions:

Name Description
calculate_layout

Compute layout for node and all descendants, in place.

extract_layout_style

Return a dict of the layout-relevant entries in props.

Attributes:

Name Type Description
LAYOUT_STYLE_KEYS

Style keys that affect layout (and are consumed by the layout engine).

LAYOUT_STYLE_KEYS module-attribute

LAYOUT_STYLE_KEYS = frozenset({'width', 'height', 'min_width', 'max_width', 'min_height', 'max_height', 'flex', 'flex_grow', 'flex_shrink', 'flex_basis', 'flex_wrap', 'align_self', 'align_content', 'position', 'top', 'right', 'bottom', 'left', 'start', 'end', 'margin', 'margin_top', 'margin_bottom', 'margin_left', 'margin_right', 'margin_start', 'margin_end', 'margin_horizontal', 'margin_vertical', 'padding', 'padding_top', 'padding_bottom', 'padding_left', 'padding_right', 'padding_start', 'padding_end', 'padding_horizontal', 'padding_vertical', 'flex_direction', 'justify_content', 'align_items', 'spacing', 'gap', 'row_gap', 'column_gap', 'aspect_ratio', 'direction'})

Style keys that affect layout (and are consumed by the layout engine).

LayoutNode

LayoutNode(style: Optional[Dict[str, Any]] = None, children: Optional[List[LayoutNode]] = None, measure: Optional[MeasureFn] = None, user_data: Any = None)

A node in the layout tree.

Holds the layout-relevant style props, child layout nodes, and the computed output (x, y, width, height in points relative to the parent's coordinate space).

Attributes:

Name Type Description
style

Dict of layout-relevant style props (a subset of the element's full props; usually filtered through LAYOUT_STYLE_KEYS).

children

Ordered list of child LayoutNodes.

measure

Optional measure callback for leaf nodes whose natural size depends on their content (e.g., text). Receives (max_width, max_height) and returns (width, height). Either argument may be math.inf.

user_data

Free-form attribute the caller may use to associate each layout node with the corresponding native view; the engine itself does not inspect it.

dirty bool

When False, the node may serve repeat measurements from its memo (set by the reconciler's incremental-layout cache). Fresh nodes start dirty.

x float

Computed x-coordinate relative to the parent's coordinate space.

y float

Computed y-coordinate relative to the parent's coordinate space.

width float

Computed width in points.

height float

Computed height in points.

calculate_layout

calculate_layout(node: LayoutNode, available_width: float, available_height: float, direction: str = DIRECTION_LTR) -> None

Compute layout for node and all descendants, in place.

Sizes the root from its own style and content. Callers that want the root to fill its viewport should wrap it in a synthetic outer node with explicit width / height (the Reconciler does this when running the layout pass after a commit).

Parameters:

Name Type Description Default
node LayoutNode

Root of the layout tree.

required
available_width float

Available width in points. Pass math.inf for unbounded (e.g., horizontal scroll).

required
available_height float

Available height in points. Pass math.inf for unbounded (e.g., vertical scroll).

required
direction str

Base writing direction ("ltr" or "rtl") inherited by nodes that don't set their own.

DIRECTION_LTR

extract_layout_style

extract_layout_style(props: Dict[str, Any]) -> Dict[str, Any]

Return a dict of the layout-relevant entries in props.

Used by the reconciler when building a LayoutNode from an element so the layout engine doesn't have to scan unrelated visual props.

Next steps