Loom logo Rust ยท flexbox layout engine from scratch

Watch a layout
solve itself.

Loom is a dependency free, retained mode GUI layout engine written in pure Rust. Give it a tree of widgets and a container size and it computes the exact border box rectangle of every node with a flexbox style constraint solver, with optional wrapping. The core is headless, so it can run and be tested without a screen. Build a tree, set flex and spacing, and drag the size slider to watch every box reflow, live.

Open the playground View on GitHub

Inspector

Add child

Rendered layout

Container width: 800 px
Container height: 520 px

Each label shows #id kind and the computed rectangle x, y   w × h. Click any box to select it. Green boxes are leaf widgets, blue boxes are containers.

Widget tree

    every number above is computed live by the in-browser solver, the same algorithm as the Rust core. Draw calls are one rect per node plus one text call per label.

    How to use this playground
    Guided tour. Start with the sample already on the stage. Click the blue toolbar row at the top to select it, then set Justify to Space between and watch its buttons spread apart. Now grab the Container width slider and drag it left and right. Every box with a flex grow reflows in real time. Select the large empty Box in the content area, give it a Flex grow of 1, and it will expand to fill the leftover space. Finally select any container and press + Text to drop a new widget inside it.

    The controls, one by one

    1. Select a node. Click a box on the stage or a row in the widget tree on the right. The inspector on the left then edits that node.
    2. Direction. For a container, choose Row to lay children left to right or Column to stack them top to bottom.
    3. Justify. Distributes leftover space along the main axis. Start, Center, End, or Space between. It only has a visible effect when the children do not already fill the axis through flex grow.
    4. Align. Positions children on the cross axis. Stretch makes an auto sized child fill the cross axis. Start, Center, and End place it instead.
    5. Gap. Fixed space inserted between children.
    6. Padding, Border, Margin. The box model. Padding and border sit inside the box and shrink the content area. Margin sits outside and pushes the box away from its siblings.
    7. Width and Height. Tick auto to size from content, or untick and type a fixed pixel size for the border box.
    8. Flex grow. A weight for sharing leftover main axis space. Two children with grow 1 split the space evenly. Grow 1 versus grow 3 splits it one to three.
    9. Flex shrink. A weight for absorbing overflow when the children do not fit. Higher shrink gives up more space.
    10. Add child. With a container selected, add a Row, Column, Text, Button, Box, or Spacer inside it.
    11. Delete node. Removes the selected node and its subtree. The root cannot be deleted.
    12. Container size sliders. Set the available width and height handed to the root, then watch the whole tree reflow.

    Reading the result

    • The rectangle on each box is its border box. It includes border and padding but not margin, exactly like the Rust engine.
    • Labels read x, y for the top left corner and w × h for the size, all in pixels.
    • The widget tree mirrors the stage. Selecting in one highlights the other.

    One question, answered exactly

    Loom separates the math from the pixels. Given a tree of widgets and a container size, it computes the exact border box rectangle of every node with a flexbox style constraint solver, with optional wrapping. There is no windowing crate, no font backend, no GPU. A Renderer trait is the only seam to the outside, so the same layout can drive a terminal, an SVG file, a canvas, or a GPU backend.

    # a spacer absorbs all leftover space on the main axis, exactly:
    root.children[1].rect.w == 200.0 - 48.0 - 64.0 - 8.0 * 2.0
    
    # golden layouts pin exact rectangles; invariants run over random trees
    # fed huge, fractional, and non finite sizes; a determinism gate proves
    # the same tree gives identical rectangles on every run

    Correctness is committed as tests that run on every build. Every child rectangle must lie inside its parent content box on any axis that fits, siblings must never overlap, no coordinate may be non finite, and equal flex children must fill the main axis within a pixel. A self check even corrupts a good layout to prove the invariant checker rejects it, so a passing gate cannot be a blind gate.

    Real output, not a mock

    This is what cargo run -- demo prints: the computed border box rectangle of every node, then the recorded draw calls, then a hit test probe. It is the same sample tree the playground starts with, so the numbers match box for box.

    $ cargo run -- demo == Computed rectangle tree == #0 Container x=0.0 y=0.0 w=800.0 h=600.0 #1 Container x=0.0 y=0.0 w=800.0 h=48.0 #2 Button "File" x=12.0 y=10.0 w=56.0 h=28.0 #4 Spacer x=140.0 y=24.0 w=568.0 h=0.0 #5 Button "Save" x=716.0 y=10.0 w=72.0 h=28.0 #6 Container x=0.0 y=48.0 w=800.0 h=552.0 #15 Box x=176.0 y=100.0 w=608.0 h=436.0 == Recorded draw calls == draw_rect #5 button (716.0, 10.0, 72.0, 28.0) draw_text #5 "Save" at (716.0, 10.0) == Hit test == point (40, 30) hits node #2 hit path: [0, 1, 2]

    The spacer absorbs every pixel of leftover space on the toolbar row, so #4 lands at width 568. Drag the size slider in the playground and watch the same solver redistribute it.

    Where Loom sits

    Loom borrows its mental model from real layout systems. It does not try to replace them. It is the small, readable, headless version you can drop in with no supply chain and read end to end.

    CSS flexbox and the box model

    The spec Loom takes its rules from: main and cross axes, grow and shrink, justify and align, border box sizing. It lives inside a browser engine, not as a standalone headless crate you can single step.

    Yoga, Taffy

    Production layout engines. Yoga is Facebook's C based flexbox engine behind React Native. Taffy is a substantial Rust crate implementing flexbox, grid, and block. Fast, feature complete, and large.

    Loom

    The readable from-scratch version. A dependency free core that answers one question, a tree plus a size in and exact border box rectangles out, headless and deterministic, with a Renderer trait as the only seam. Small enough to read in a sitting, backed by golden and invariant gates.

    The concepts that matter

    The layout primitives Loom implements.

    Each of these is a real part of the solver in src/, not a marketing word.

    model Border box model

    Every rect is the border box: it includes border and padding but excludes margin, exactly like the CSS box model. A fixed width or height sets the border box size, and content lives inside border plus padding.

    flex Grow and shrink

    Leftover main axis space is shared by grow weight, and overflow is absorbed by shrink weight scaled by base size. Two equal grow children split the space evenly, one to three splits it one to three.

    clamp Min and max bounds

    min_width, max_width, min_height, and max_height bound the resolved size on each axis. An unset bound means no limit, and when a minimum exceeds a maximum the minimum wins.

    wrap Wrapping and per-line justify

    With wrapping on, children break onto a new line when the current line fills, an oversized child gets its own line, and justify and align apply per line, not once for the whole container.

    seam The Renderer trait

    The one seam to the outside. Layout emits draw calls to any Renderer, so the same computed tree can drive a terminal, an SVG file, a canvas, or a real GPU backend without touching the solver.

    gate Determinism and finite output

    The same tree at the same size produces identical rectangles on every run, and any NaN or infinity handed in is normalized before the solver runs, so every coordinate the engine produces is finite.

    Use it

    Loom is a runnable CLI, a dependency free library with a fluent tree builder, and a correctness gate you can run yourself. The whole core is headless, so none of it needs a screen.

    CLI

    Print the computed rectangle tree at any container size, or run demo to also print the recorded draw calls and a hit test probe.

    Library API

    Build a tree with Node::row() and Node::column() and a fluent builder, then assign_ids, compute_layout, render to any Renderer, and hit_test or hit_path for input.

    Correctness gates

    Golden layouts pin exact hand computed rectangles, invariants run over random trees fed degenerate sizes, a determinism gate proves stability, and a self check corrupts a layout to prove the checker is not blind. All green under cargo test.

    # print the computed rectangle tree at 800 x 600
    cargo run -- 800 600
    
    # also print draw calls and a hit test probe
    cargo run -- demo
    
    # run the correctness gates and unit tests
    cargo test
    cargo clippy --all-targets -- -D warnings
    LOOM_FUZZ_OPS=5000 cargo test invariants