Red Otter logo
Red Otter
0.1.15

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


interface

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.

Field
Type and description
_state
LayoutNodeState

State of the node updated by the layout engine.

_style
ExactLayoutProps
firstChild
Node
lastChild
Node
next
Node
parent
Node
prev
Node
testID
string

function

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.

Parameter
Type and description
ui
Renderer
node
Node
clipStart
Vec2
clipSize
Vec2
scrollOffset
Vec2
returns
void

Type declaration

TypeScript
(ui: Renderer, node: Node, clipStart?: Vec2, clipSize?: Vec2, scrollOffset?: Vec2) => void

function

isMouseEvent

/layout/eventTypes.ts
Parameter
Type and description
event
UserEvent
returns
boolean

Type declaration

TypeScript
(event: UserEvent) => event is MouseEvent

/layout/eventTypes.ts
Parameter
Type and description
event
UserEvent
returns
boolean

Type declaration

TypeScript
(event: UserEvent) => event is KeyboardEvent

function

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.

Parameter
Type and description
tree
Node

tree of views to layout.

fontLookups
Lookups

used for calculating text shapes for text wrapping. Can be null if not needed.

rootSize
Vec2

size of the root element.

returns
void

Type declaration

TypeScript
(tree: Node, fontLookups: Lookups, rootSize: Vec2) => void

function

paint

/layout/paint.ts

Takes a renderer and a root of a tree and commands the renderer to paint it. Used every frame.

Parameter
Type and description
ui
Renderer

renderer instance that will get commands issued to it.

node
Node

root of the tree to paint.

returns
void

Type declaration

TypeScript
(ui: Renderer, node: Node) => void

/layout/BaseView.ts

Basic building block of the UI. A node in a tree which is mutated by the layout algorithm.

Field
Type and description
testID
string
next
Node
prev
Node
firstChild
Node
lastChild
Node
parent
Node
_state
LayoutNodeState

Internal state of the node. It's public so that you can use it if you need to, but it's ugly so that you don't forget it might break at any time.

_style
Required<Omit<DecorativeProps, "borderRadius">> & Required<Omit<LayoutProps, "aspectRatio" | "bottom" | "flexBasis" | "height" | ... 17 more ... | "zIndex">> & { ...; }

Should always be normalized.

method

add

Parameter
Type and description
node
Node
returns
Node

Type declaration

TypeScript
(node: Node) => Node;
method

remove

Parameter
Type and description
node
Node
returns
void

Type declaration

TypeScript
(node: Node) => void

class

Text

/layout/Text.ts

Basic text node. The only way to create text. It cannot have children.

Field
Type and description
testID
string
next
Node
prev
Node
firstChild
Node
lastChild
Node
parent
Node
_style
TextStyleProps & Required<Omit<LayoutProps, "aspectRatio" | "bottom" | "flexBasis" | "height" | "left" | "margin" | "marginHorizontal" | "marginVertical" | ... 13 more ... | "zIndex">> & { ...; }

Should always be normalized.

_state
LayoutNodeState

State of the node updated by the layout engine.


class

View

extends BaseView
/layout/View.ts

BaseView but with event listeners.

Field
Type and description
_eventListeners
UserEventTuple[]
_isMouseOver
boolean

Controlled by EventManager. Needed for dispatching mouseEnter and mouseLeave events.

_scrolling
{ xActive: boolean; xHovered: boolean; yActive: boolean; yHovered: boolean; }

Want to learn more?

I wrote a blogpost about implementing similar algorithm – How to Write a Flexbox Layout Engine.


Copyright © Tomasz Czajęcki 2023