Ooidget Component Format 0.1Editor's Draft
Layout
The algorithm in this chapter is normative as code: layout.js
in the web runtime, plain JavaScript written to be translated line for line. A runtime in another language translates
it (the Rust core does, function for function) and MUST place every box it places: the conformance suite’s random
layout cases, generated from it, hold every runtime to that. Where this prose and that file differ, the file is
right, and this prose is wrong.
Absolute placement
A widget without layout places each child by its frame or frameItem (computed as a
frame inside the widget’s box) or, without one, at x, y at the child’s design size.
Flex layout
A widget’s layout arranges its children in a row or a column, with the semantics of a CSS flex container
[CSS-FLEXBOX-1], or in a grid (below). The properties are those a design tool’s
layouts have (Penpot’s flex and grid layouts map onto them one to one), written as CSS writes them:
| Property | Type | CSS equivalent | Description |
|---|---|---|---|
dir | string | flex-direction | "row" (default) or "column". |
gap | number or [row, column] | gap | Space between adjacent children; two values for rows and columns apart. Absent: 0. |
pad | number, [v, h] or [t, r, b, l] | padding | Space inside the container’s edges: its margin from them. Absent: 0. |
align | string | align-items | "stretch" (default), "start", "center", "end". |
justify | string | justify-content | "start" (default), "center", "end", "space-between", "space-around", "space-evenly". |
wrap | boolean | flex-wrap: wrap | Children that do not fit move to a new line. Absent: one line. |
grid | object | display: grid | { columns, rows }: tracks, each a number (fixed), "Nfr" (a share of what is left), "N%" or "auto" (as wide as its widest child). Children fill the cells in turn, or take the one their cell names. |
Items
| Property | Type | CSS equivalent | Description |
|---|---|---|---|
basis | Expression<number> | flex-basis | Main size before growing. Absent: the child’s design size on the main axis. |
flex | number | flex-grow | Share of the free space. Absent: 0. |
min, max | number | min-width / max-width (or heights) | Clamp of the main size after growing. |
align | string | align-self | Overrides the container’s align. |
margin | number, [v, h] or [t, r, b, l] | margin | Space kept around the child, on top of the gaps. |
aspect | number | aspect-ratio | Width over height: a stretched child takes its main size from its cross size (1: square). |
cell | object | grid-column, grid-row | In a grid: { column, row, columnSpan, rowSpan }, lines counted from 1. |
importance | number | How long the item keeps its room when the items do not fit, against its siblings: the least important gives way first (see Collapsing). Absent: it never gives way. | |
yield | string | How it gives way: "fade" (default), drawn smaller and fainter at its own size; or "shrink", laid out smaller down to its min, then fading. |
Children are not shrunk below their basis (flex-shrink: 0). Children whose when is false take no space. The
cross size of a non-stretched child is its design size on the cross axis.
A runtime MUST produce the boxes CSS would produce for an equivalent flex container whose items have the given
flex-basis, flex-grow, flex-shrink: 0, min-*, max-* and align-self, then apply collapsing.
Collapsing
When the items’ sizes after growing, with their gaps, exceed the container’s main size, the excess is taken from the
items that have an importance, least important first (array order among equals). Importance is relative to the
item’s siblings only: each container ranks its own parts. An item with yield: "shrink" first gives up the part of
its main size above its min, laid out smaller. Then it gives up its room, and the gap beside it (the one before it,
or after it when it is first) in proportion, continuously, until it has none; only then does the next start. An item
left with a share
f of its room (0 < f < 1) is laid out at its own size, centred on what it has left, and drawn scaled by f
about that centre while it fades: with the item’s minScale m (0.7 when absent), its opacity is
((f − m) ⁄ (1 − m))², and at f = m it is gone. Its room goes on shrinking to nothing for its siblings to
take, so a part is never shown smaller than m of its size and never overlaps another. Below f = ½ it takes no
input.
When even that leaves too much, every item still there shrinks in proportion, below its min if it must: items
never overlap one another. (Where CSS would let them overflow, Ooidget does not.)
So a component sheds its least important parts one at a time as its room shrinks, and takes them back in reverse, with no jumps: the same size always gives the same picture, and a resize animates by itself.
Adapting to the container
A root instance in a host area that the host resizes (a window, a panel, a plug-in editor) adapts in three steps. The widget declares its limits:
| Property | Type | Description |
|---|---|---|
minSize | [w, h] | The smallest layout it works at, in design units. Below it, the picture is scaled down whole. |
maxSize | [w, h] | The largest layout. Past it the layout stops growing, and the widget sits in the middle of its area. |
scaling | object | { min, max, base? }: how far everything grows or shrinks together. Absent: it does not. |
aspect | object | { min?, max? }: the proportions it keeps, width over height. In an area too tall or too wide, the long side of the layout is cut back to the nearest allowed proportion, and the widget sits in the middle. |
Every one of these is the author’s choice: a widget that declares none of them is laid out at its area’s size and
nothing in it scales, shrinks or hides; one that wants a fixed picture declares minSize and maxSize equal to its
size. Only parts given an importance ever give way.
- Scale. The runtime finds the factor k = √((W ⁄ basew) × (H ⁄ baseh)) for an area of W × H, clamped to
[
min,max]; base isscaling.base, else the widget’ssize. Type, controls, strokes and displays are all in design units, so they all grow and shrink by k together, in proportion, as design tokens scaled as one. - Reflow. The layout is computed at W ⁄ k × H ⁄ k design units, held within
minSize,maxSizeandaspect: flex items grow, frames anchored to edges follow them, and expressions read the new size asui.wandui.h. - Collapse. Where even the minimums do not fit, parts give way by importance.
The picture is shown at k (or smaller, when held at minSize in too small an area). These steps are continuous
in the area’s size: there are no breakpoints, and growing back retraces shrinking.
The model follows CSS: steps 1 and 2 are what clamp() sizes and a flex container give a page; collapsing is the
part CSS has no property for, and is written here so that every runtime sheds parts in the same order.
Size classes remain for designs that are different, not just smaller, at a size.
Size classes
A root instance is shown in an area of the host’s size. The runtime sets env.sizeClass
from the area’s width in design units:
| Size class | Width | Typical device |
|---|---|---|
watch | below 240 | watches, round or rectangular |
compact | 240 to below 600 | phones |
regular | 600 to below 1024 | tablets, small windows |
wide | 1024 and above | desktop windows |
spatial | set by the host | panels in a 3D space |
A component adapts to size by conditions on env in when, in frames and in expressions. A component MAY also give
variants, whole widgets for a size class, in extensions.OOIDGET_variants (see Extensions).