Metadata-Version: 2.4
Name: yogalayout
Version: 0.2.0
Summary: RAII Python bindings for the Yoga flexbox layout engine
License-Expression: MIT
License-File: LICENSE
License-File: THIRD_PARTY_LICENSES.md
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# yogalayout

RAII Python bindings for [Yoga](https://www.yogalayout.dev/) (v3.2.1, public C API only).

## Quick start

```python
from yogalayout import Config, Node

cfg = Config(point_scale_factor=1)  # 1 = whole-pixel layout (0 disables rounding)
root = Node(
    cfg,
    width=250,
    height=475,
    padding=10,
    children=[
        Node(
            cfg,
            flex=1,
            row_gap=10,
            children=[
                header := Node(cfg, height=60),
                a := Node(cfg, flex=1, margin_inline=10),
                b := Node(cfg, flex=2, margin_inline=10),
            ],
        ),
    ],
)
root.calculate_layout()  # width/height default to the root's own size
assert tuple(a.layout) == (10, 70, 210, 125)  # (left, top, width, height), relative to the parent
```

* **Style** is set with keyword arguments or properties: numbers are points, `"50%"` is a percentage,
  `"auto"` and `None` (undefined) work where Yoga accepts them. Enums take strings
  (`flex_direction="row"`, spelling-tolerant: `"no-wrap"`, `"NO_WRAP"`, `"nowrap"`) or the enum members.
  CSS-like shorthands: `padding=(vertical, horizontal)`, `margin_inline=...`, `gap=`, `row_gap=`,
  `position="absolute"` with `top=`/`left=`/... insets. Per-edge setters: `node.set_padding(Edge.TOP, 4)`.
* **Layout** results: `node.layout` is `Layout(left, top, width, height)` (also `right`, `bottom`);
  `layout_margin/padding/border(edge)`, `had_overflow`, `has_new_layout`.
* `Config(use_web_defaults=..., point_scale_factor=..., errata=...)` must be set up **before** nodes use it.

### Measure functions

Leaves can size themselves (text, images) with a callback that gets the available space:

```python
from yogalayout import MeasureMode


def measure(width, width_mode, height, height_mode):
    return (80.0, 20.0)  # (width, height); NaN or negative raises ValueError


root = Node(width=200, height=100, flex_direction="row", children=[Node(measure=measure)])
root.calculate_layout()
```

`node.set_baseline(fn)` and `node.set_dirtied(fn)` work the same way. An exception raised in a callback
is re-raised, with its original type, by the call that triggered it (`calculate_layout()`, or the
setter / `mark_dirty()` for `dirtied`).

### Children and lifetimes

```python
root = Node()
kids = [Node(), Node()]
root.set_children(kids)  # replaces all children atomically
root.insert_child(Node(), 0)
root.remove_child(kids[0])
assert kids[0].parent is None
```

A parent keeps its children alive; a child only refers to its parent weakly. Dropping the parent
frees the whole tree deterministically, nodes you still hold stay valid (they just lose their
parent), and reference cycles made by closures are collected. Every mistake Yoga would turn into a
process abort (a node with two parents, inserting a node into itself, children under a measured
leaf, a bad index) raises `ValueError`/`IndexError` instead.

## Install

```
pip install yogalayout
```

Wheels for Linux (x86_64, aarch64), macOS (x86_64, arm64) and Windows (amd64): one abi3 wheel for
Python 3.12+, wheels for 3.10/3.11, and a free-threaded 3.14t wheel.

## Thread safety

Free-threaded CPython (3.14t) is supported; on GIL builds the same rules keep threads from
corrupting each other.

* All nodes of one tree share a lock and every method takes it. **Different trees run in
  parallel; one tree is used by one thread at a time** (other threads wait, with the GIL
  released), so you never need your own lock around a tree.
* While `calculate_layout()` (or a style change, which can fire the `dirtied` callback) runs on a
  tree, **changing that tree raises `RuntimeError`**. This includes calling `calculate_layout`
  again, inserting/removing nodes and setting styles or callbacks from inside a measure, baseline
  or dirtied callback. Reading (`node.layout`, `children`, `parent`, style getters) is allowed.
  Layout of a *different* tree from a callback is fine.
* An exception raised by a callback is re-raised, with its original type, by the call that caused
  it, in the thread that made the call. A measure function returning NaN/negative sizes, or a
  baseline function returning NaN (which would abort inside Yoga), raises `ValueError`.
* A `Config` is frozen once a node uses it: set `point_scale_factor`, `errata` etc. before
  creating nodes.
* Reparenting moves a whole subtree to the new tree's lock, so a `Node` may be handed between
  threads freely.

## More

* [`docs/HANDOVER.md`](docs/HANDOVER.md): design, dev setup, status and next steps.

## License

MIT (see `LICENSE`). Yoga itself is also MIT-licensed; see `THIRD_PARTY_LICENSES.md`.

## Tests

`tests/conformance/` is generated by `tools/gen_conformance.py` from Yoga's own generated layout
tests (`extern/yoga/tests/generated`, MIT, Meta Platforms) so the binding is checked against
upstream's expected numbers. Regenerate after bumping the Yoga submodule.

## Not bound

`YGNodeClone`/`CopyStyle`/`Reset`/`Finalize`/`FreeRecursive` (ownership is handled by Python references),
the config logger (the only messages Yoga logs are failed asserts, which are checked up front, and
invalid measure results, which are rejected), config context pointers, and `YGNodeSwapChild` (use
`set_children`).
