@urvis/layout
v0.1.3
Published
From-scratch CSS-flexbox layout for the urvis scene graph. Tree-native, synchronous, no WASM, no dependencies. Augments `@urvis/core` `DisplayObject` with an optional `layout?: LayoutProps` via TypeScript declaration merging so `@urvis/core` stays free of
Readme
@urvis/layout
From-scratch CSS-flexbox layout for the urvis scene graph. Tree-native,
synchronous, no WASM, no dependencies. Augments @urvis/core DisplayObject
with an optional layout?: LayoutProps via TypeScript declaration merging so
@urvis/core stays free of layout concepts.
Install
bun add @urvis/layoutConcepts
- Opt-in. Only nodes with
layout.display = "flex"participate. All others are ignored. - Position-only.
computeLayoutwritesx/yon leaf nodes and a read-onlycomputed: { width, height }box on every laid-out node. It never deforms geometry. A resizable node (e.g. aRectpanel) reads its ownnode.layout.computedand applies the size explicitly. - Default cross-axis align is
"stretch"(CSS default). A leaf'scomputedcross-size fills its line unlessalignoralignSelfsays otherwise. The shape's geometry is unchanged regardless. - Flex subtree only.
computeLayoutwalks an unbroken chain ofdisplay:"flex"containers fromroot. See Limitations.
Usage
import { Group, Rect } from "@urvis/core";
import { computeLayout } from "@urvis/layout";
const root = new Group();
root.layout = {
display: "flex",
direction: "row",
gap: 8,
padding: 16,
align: "center",
};
const a = new Rect({ width: 0, height: 0 }); // sized by layout
a.layout = { grow: 1, basis: 0 };
const b = new Rect({ width: 0, height: 0 });
b.layout = { basis: 120 };
root.append(a, b);
// Lay out into a 400×300 viewport.
computeLayout(root, { width: 400, height: 300 });
// Nodes are now positioned.
console.log(a.x, a.y); // e.g. 16, …
console.log(a.layout?.computed); // { width: 240, height: 268 }
console.log(b.layout?.computed); // { width: 120, height: 268 }
// A panel Rect reads computed to apply the real size:
// a.width = a.layout!.computed!.width;
// a.height = a.layout!.computed!.height;API
computeLayout(root, opts?)
function computeLayout(
root: DisplayObject,
opts?: { width?: number; height?: number },
): voidWalks the connected flex subtree from root and positions every node
reachable through an unbroken chain of display:"flex" containers.
Synchronous; writes x/y and layout.computed in place.
Root box resolution priority:
opts.width/opts.heightroot.layout.width/root.layout.heightroot.localBounds()(0 for a bareGroup)
LayoutProps
Attach to node.layout. All fields are optional.
Container fields (control how children are arranged):
| Field | Type | Default |
| -------------- | --------------------------------------------------------------------------------------- | ------------ |
| display | "flex" | — |
| direction | "row" \| "row-reverse" \| "column" \| "column-reverse" | "row" |
| justify | "start" \| "end" \| "center" \| "space-between" \| "space-around" \| "space-evenly" | "start" |
| align | "start" \| "end" \| "center" \| "stretch" | "stretch" |
| alignContent | "start" \| "end" \| "center" \| "stretch" \| "space-between" \| "space-around" | "stretch" |
| wrap | "nowrap" \| "wrap" \| "wrap-reverse" | "nowrap" |
| gap | number \| [row, col] | 0 |
| padding | Sides | 0 |
Item fields (control how a node participates in its parent container):
| Field | Type | Default |
| ----------- | ------------------------------------------------- | ---------- |
| grow | number | 0 |
| shrink | number | 1 |
| basis | Dim | "auto" |
| alignSelf | "auto" \| "start" \| "end" \| "center" \| "stretch" | "auto" |
| margin | Sides | 0 |
| width | Dim | "auto" |
| height | Dim | "auto" |
| minWidth | Dim | — |
| maxWidth | Dim | — |
| minHeight | Dim | — |
| maxHeight | Dim | — |
Output field (written by computeLayout, treat as read-only):
| Field | Type |
| ---------- | --------------------------- |
| computed | { width: number; height: number } |
Dim
type Dim = number | `${number}%` | "auto";Pixels, a percentage of the container content box, or auto.
Sides
type Sides = number | { top?: number; right?: number; bottom?: number; left?: number };One value for all four edges, or per-side overrides.
Limitations
1. Nested flex containers need an explicit or flexible main size
Intrinsic size comes from node.localBounds(). A bare Group returns null
(treated as 0), so a nested flex Group with no grow, basis, or explicit
width/height collapses to 0 on the main axis and its children pile at the
origin. Give nested flex containers grow, a basis, or an explicit main
size. The engine does not shrink-to-fit content.
// Bad — inner collapses to 0 wide:
inner.layout = { display: "flex", direction: "column" };
// Good — let it grow to fill available space:
inner.layout = { display: "flex", direction: "column", grow: 1 };2. align: "stretch" overrides an item's explicit cross size
An item that sets a cross-axis size (height in a row / width in a column)
still has its computed cross dimension stretched to fill the line under the
default align: "stretch". CSS would preserve the explicit size; this engine
does not yet. Geometry is never deformed, so this only affects the reported
computed box (and a nested container's recursion box). Planned follow-up: a
per-item explicit-cross flag.
Workaround: set alignSelf: "start" (or any non-stretch value) on the
item.
3. Grid is out of scope
This version implements flexbox only. CSS Grid is not planned for this release.
