Layout engine
The layout algorithm is perhaps the most valuable and unique part of the library and took the most time to develop.
Overview
Layout engine executes a 4 pass algorithm:
- Pass 1: Traverse tree in level order and generate the reverse queue.
- Pass 2: Going bottom-up, level order, resolve sizes of elements that base their size on their children.
- Pass 3: Going top-down, level order, resolve flex sizes by splitting available space between children and assign positions of all elements based on specified flex modifiers.
- Pass 4: Going top-down, level order, calculate scroll sizes – the actual space used by children versus available component size.
What is actually happening?
Pass 1
Internal state of each node is reset (see LayoutNodeState).
If element has defined width or height, it is applied. If it is a text node and parent has defined width, the maximum available width of the text is now known.
Pass 2
If element has undefined size on the main axis (width for row, height for column), it is calculated as a sum of element’s paddings, widths and margins of all children. For cross axis it is the maximum value of children sizes (and element’s paddings).
The children are divided into rows. If flexWrap
is not defined it will be a single row. If it is defined, sizes of all children are calculated and rows are split based on them and gap values.
Pass 3
If element has both left and right offsets (or top and bottom) it is used to set its size.
For each row of children, each child is positioned based on alignContent
, alignItems
, justifyContent
and alignSelf
properties. Also flex sizes are determined based on flexGrow
, flexShrink
and flexBasis
properties. Min, max sizes and aspect ratio is applied.
Size and position of each element is rounded using Math.round()
to a full pixel.
pass 4
Scroll size of each element is calculated, which is the maximum area needed to display all children. Used for scrolling.
Quick story
My initial idea was to base it off the Auto Layout system from Figma, but it soon turned out that the CSS flexbox API is more familiar to write. Since there was a similar already successfully developed project by Facebook, Yoga, I decided to follow the same subset of implemented flexbox features.
I made first implementation in Zig in June 2022. In July I came with with the a 3-pass tree traversal that allows to resolve all flexbox properties without introducing any recursion. I released Red Otter in early 2023. At the time it was missing flex wrap and scrollable containers.
I spent a lot of time thinking how approach interactivity. In October 2023 I settled for retained mode UI rendering and what soon followed was a major rewrite of the layout algorithm that enabled flex wrap and shortened the code. In November I finished the implementation and implemented scrolling, which required rethinking other execution layers of the library.
API
Node
/layout/Node.ts ↗
Basic node in the layout tree. Containing its state and style information as well as pointers to its children, siblings, and parent.
compose
/layout/compose.ts ↗
Takes tree of nodes processed by layout()
and calculates current positions based on
accumulated scroll values and calculates parent clipping rectangle.
Type declaration
(ui: Renderer, node: Node, clipStart?: Vec2, clipSize?: Vec2, scrollOffset?: Vec2) => void
isMouseEvent
/layout/eventTypes.ts ↗
Type declaration
(event: UserEvent) => event is MouseEvent
isKeyboardEvent
/layout/eventTypes.ts ↗
Type declaration
(event: UserEvent) => event is KeyboardEvent
layout
/layout/layout.ts ↗
This function traverses the tree and calculates layout information - width
, height
, x
, y
of each element - and stores it in __state
of each node. Coordinates are in pixels and start
point for each element is top left corner of the root element, which is created around the tree
passed to this function. What this means in practice is that all coordinates are global and not
relative to the parent.
Type declaration
(tree: Node, fontLookups: Lookups, rootSize: Vec2) => void
paint
/layout/paint.ts ↗
Takes a renderer and a root of a tree and commands the renderer to paint it. Used every frame.
Type declaration
(ui: Renderer, node: Node) => void
BaseView
/layout/BaseView.ts ↗
Basic building block of the UI. A node in a tree which is mutated by the layout algorithm.
Want to learn more?
I wrote a blogpost about implementing similar algorithm – How to Write a Flexbox Layout Engine.