The Life of a Programmer

Core Layout Protocol API (UI Engine)

Let’s get into some technical details about how a layout engine works. We assume that we have a tree of elements that comprise the UI. Starting from the root node, we request a layout of all the elements of the tree. This is a recursive process, ideally with just one pass through each node.

This article covers the breadth of the API rather than going too deeply. We’ll need to go over several examples later to understand better what happens.

This article is part of a series on Writing a UI Engine.

GetMarginSize

Nodes in the tree need to measure themselves when asked. A layout uses this call to position elements relative to each other.

float2 GetMarginSize(LayoutParams lp)

At this level, in node layout, we concern ourselves only with the margin-box. The details of the box-model are something left to individual elements when performing their layout.

When called this function, given the LayoutParams provide the desired size of the node. This can be called as often as desired, but each call could be expensive, especially for deep trees of dependently sized elements. We’ll talk about how LayoutParams helps a bit later.

ArrangeMarginBox

Once the node determines its size, it must also provide a way to arrange itself, and it’s children. This is done via a ArrangeMarginBox function.

float2 ArrangeMarginBox(LayoutParams lp)

The signature is curiously the same as for GetMarginSize due to a key optimization in layout: many nodes don’t need to know the size of the child before arranging it — no call to GetMarginSize is required to perform their layout. The parent can instead pass the LayoutParams to ArrangeMarginBox and get the resulting size back — some of them don’t even need this size.

Position and the Fuse Signature

ArrangeMarginBox does not provide the intended position of the child. The size and layout of a node are expected to be independent of its position on the screen. Position independence is a critical requirement if you intend on allowing animation of nodes.

Layout being dependent on position would mean that a control positioned at (10,10) would have a different size and child arrangement than the same control positioned at (50,30). In practice, I’ve never found a situation where this would be helpful. Aligning something left/right makes a difference, but those are distinct layout properties, thus expected to make a difference. Position independence refers to a change in position when all other layout parameters are identical: alignment flags, available space, box spacing, etc.

Alas, when this function was born in Fuse the understanding of position independence wasn’t there yet. Thus we had this signature:

public float2 ArrangeMarginBox(float2 position, LayoutParams lp)

I worked around this signature and built the Element node hierarchy (where all the actual controls lived), to be position independent. I made a few virtual functions and had derived classes implement OnArrangeMargin box instead of ArrangeMarginBox directly. I managed to make Fuse’s controls position independent, and enjoy the optimization it brings but was stuck with an odd interface for backwards compatibility.

LayoutParams

The one-pass layout goal I mentioned, which reduces node passes, requires each node have sufficient information about the layout requirements. This is where the LayoutParams comes in.

The Ugly that came before

First, let’s look at the source of multiple layout passes before to using LayoutParams. In Fuse, I initially inherited a simpler WPF model, which provided only the available-size and expand flags to the measure functions, something like Measure( float2 size, SizeFlags flags).

An element has properties like MaxWidth and MinWidth that apply limits to the size. These caused issues for flowing elements, like text. Applying the maximum required the parent element to first Measure the child then constrain to the maximum. Since a child text might flow differently, we’d call Measure again with a new constrained size — the height of the text changes as the available width changes. The same thing happens with any dependently sized element, like wrapping layouts, and some images.

It led to a problem where arranging children could involve many calls to Measure. It gets even worse when a layout, like Grid, makes additional sizing decisions, invoking even more calls.

For additional insanity, the original Measure, as in WPF, was not an immutable function: it’d modify the control! The new GetMarginSizeis correctly an immutable call.

The fix

LayoutParams fixes the issue by providing all the relevant information to GetMarginSize, and ArrangeMarginBox. All potentially applicable constraints are encoded directly in this structure. The basic values are:

  • X, Y: The desired X and Y size for the control. These values optional and only set if the desired size is not known. When determining the natural size they will remain unset.
  • MaxX, MaxY: The maximum allowed values for X and Y.
  • MinX, MinY: The minimum allowed values for X and Y.

These allow a control like Text to wrap the text even when computing a natural size. Consider this example:

Panel
    Rectangle {Background=Blue Alignment=Center Padding=5}
        Text {Wrapping=Wrap Value="Variable length text...perhaps quite long"}

If the text fits on one line, we expect a centred blue box with the text in it. If the text is wider than the Panel however, we expect it to wrap within that width and span multiple lines. The call to Text.GetMarginBox will thus not have an X or Y, value, but will have a MaxX value. We’re capable of getting the correct text sizing with only a single pass through the children.

LayoutParams increases the difficulty of writing a GetMarginBox function. The performance improvement more than makes up for this. In practice, once the terminology and meanings are defined, it’s also not that difficult to write the function. Most controls tend to inherit most of their sizing from other controls, or using common modules. Thus you don’t implement the function too often.

Additional values can make their way into the LayoutParams type. One common request was to have relative sizes, such as Width=20% be relative to their logical container, rather than the available space. Consider such a dock layout:

DockPanel
    Panel {Width=20% Dock=Left}
    Panel {Width=20% Dock=Right}

We expect both columns to have the same width. This could be a problem since the available space, X and Y, will be different for the right column as the left one has already taken up some space. For this requirement I added a RelativeX and RelativeY to LayoutParams and defined percentages relative to that instead.

Fuse also had a Temporary flag in the LayoutParams, needed by a LayoutAnimation and Resize animation that would rapidly change the size of an element. Marking these temporary would avoid some caching, and skip over some layout: the intermediate states weren’t necessarily valid, but it looked fine and ran smoothly. Such a flag can also be used to fudge otherwise expensive text layout calculations.

Recursive forwarding

LayoutParams needs to capture the current layout constraints from the root of the tree down to the current node. Properties like maximum and minimum can vary at any point in the tree, and the current value needs to make its way down to each node.

A property like RelativeX however does not survive more than one level. Some properties are also explicitly cleared, such as the available height when arranging a vertical stack panel.

As we work through examples later we can better see how this passing of LayoutParams works. If you’re writing a new engine, try to incorporate this logic as soon as possible — it was quite difficult to retrofit into an existing system.

Obviously more details required

This article is meant to introduce the general protocol. We’ll need to go over some more details, or perhaps examples, to figure out how this all fits together. You should nonetheless get an idea of how the protocol works, so that we can refer to this logic at a later time.

This protocol is a key part of getting an optimized and efficient layout engine that supports animation. The next most important part is the invalidation engine, which I’ll get to shortly.

Please join me on Discord to discuss, or ping me on Mastadon.

Core Layout Protocol API (UI Engine)

A Harmony of People. Code That Runs the World. And the Individual Behind the Keyboard.

Mailing List

Signup to my mailing list to get notified of each article I publish.

Recent Posts