Your own Layout Controller
Controllers that aren't flexbox, how they sit inside flexbox and flexbox inside them, and how to write one.
A different tree
Flexbox covers most screens, but some layouts don't fit it, like a ring of buttons, a radial menu or a grid with its own packing rules. For those you can write your own controller, and a container that uses it places its children by your rules instead.
Your controller doesn't replace the flexbox around it, but works alongside it. Nested flex layouts share one Yoga tree, and your controller can't join that tree, so it starts a tree of its own. To the flex layout around it, that whole tree looks like a single leaf, the same as a label. The parent measures it when its size isn't fixed, and the tree then lays out its children inside the box it gets.
Here's an example with both kinds of tree side by side. On the left, three flex layouts are stitched into one tree, and
on the right, a ring uses a RadialLayoutController and is sized by its flex parent as one box:

var radius = Remember(10f);
var ring = new Layout {
LayoutController = new RadialLayoutController {
sRadius = radius
},
FlexItem = {
sSize = radius.Map(x => new YogaVector((x * 2f + 6f).pt))
},
Children = {
new Label { Text = "1" },
new Label { Text = "2" },
...
}
};Drag the slider and the numbers spread out or pull together, while the flex panel next to them stays where it is. Only the ring places its numbers again, and it sits in a slot sized for its largest radius, so nothing outside that slot has to move. To the slot, the ring is an ordinary flex item, and only its own children are placed on a circle.
The controller
A controller implements ILayoutController. Below you can see the radial controller in full, which places
its children on a circle around the center of whatever box it's given:
class RadialLayoutController : ILayoutController {
public float Radius {
get => _radius;
set {
_radius = value;
LayoutControllerUpdatedEvent?.Invoke();
}
}
public int ChildCount => _children.Count;
public event Action? LayoutControllerUpdatedEvent;
private readonly List<ILayoutItem> _children = new();
private readonly List<LayoutDimensions> _dimensions = new();
private float _radius = 10f;
private Vector2 _size;
private Vector2 _bounds;
// No context type, so a parent never stitches this controller into its
// tree and measures it instead
public Type? ContextType => null;
public object CreateContext() => new object();
public void ProvideContext(object? context) { }
public void InsertChild(ILayoutItem child, int index) {
_children.Insert(index, child);
LayoutControllerUpdatedEvent?.Invoke();
}
public void RemoveChild(ILayoutItem child) {
_children.RemoveAll(x => x.EqualsToLayoutItem(child));
LayoutControllerUpdatedEvent?.Invoke();
}
public void RemoveAllChildren() {
_children.Clear();
LayoutControllerUpdatedEvent?.Invoke();
}
public bool HasChild(ILayoutItem child) => _children.Exists(
x => x.EqualsToLayoutItem(child)
);
public void UpdateChild(ILayoutItem item) { }
public void UpdateSelf(ILayoutItem item) { }
public void Recalculate(Vector2 constraints) {
_dimensions.Clear();
var centers = new Vector2[_children.Count];
var sizes = new Vector2[_children.Count];
var min = Vector2.zero;
var max = Vector2.zero;
// Place the children around a center at zero and track the box they fill
for (var i = 0; i < _children.Count; i++) {
sizes[i] = _children[i].Measure(
width: float.NaN,
widthMode: MeasureMode.Undefined,
height: float.NaN,
heightMode: MeasureMode.Undefined
);
var turn = i / (float)_children.Count;
var angle = turn * Mathf.PI * 2f - Mathf.PI / 2f;
var direction = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
centers[i] = direction * _radius;
var from = centers[i] - sizes[i] / 2f;
var to = centers[i] + sizes[i] / 2f;
if (i == 0) {
min = from;
max = to;
} else {
min = new Vector2(Mathf.Min(min.x, from.x), Mathf.Min(min.y, from.y));
max = new Vector2(Mathf.Max(max.x, to.x), Mathf.Max(max.y, to.y));
}
}
var bounds = max - min;
var placing = constraints.x > 0f && constraints.y > 0f;
// A child that changed inside its own tree doesn't make the parent
// measure this ring again, so the ring asks for it when its box changes
if (placing && bounds != _bounds) {
LayoutControllerUpdatedEvent?.Invoke();
}
_bounds = bounds;
_size = placing ? constraints : bounds;
// Center the children's box inside the box this controller is given
var shift = (_size - bounds) / 2f - min;
for (var i = 0; i < _children.Count; i++) {
var corner = centers[i] - sizes[i] / 2f + shift;
_dimensions.Add(new LayoutDimensions(
TopOffset: corner.y,
LeftOffset: corner.x,
Width: sizes[i].x,
Height: sizes[i].y
));
}
}
public LayoutDimensions GetSelfDimensions() {
return new LayoutDimensions(0f, 0f, _size.x, _size.y);
}
public LayoutDimensions GetChildDimensions(ILayoutItem child) {
var index = _children.FindIndex(x => x.EqualsToLayoutItem(child));
return index >= 0 && index < _dimensions.Count
? _dimensions[index]
: default;
}
// How the flex parent asks this tree for its size when it isn't fixed
public Vector2 Measure(
float width,
MeasureMode widthMode,
float height,
MeasureMode heightMode
) {
Recalculate(Vector2.zero);
return LayoutTool.MeasureNode(
preferredSize: _size,
width: width,
widthMode: widthMode,
height: height,
heightMode: heightMode
);
}
}A controller has a few jobs, and its members are grouped by them:
- Computing the layout:
Recalculatemeasures the children and decides their dimensions and its own size, without applying anything. The container then reads the results throughGetSelfDimensionsandGetChildDimensions. - Being measured: a flex parent calls
Measurewhen this tree's size isn't fixed. - Tracking children:
InsertChild,RemoveChildand the other child members are bookkeeping. Compare items withEqualsToLayoutItemrather than by reference, because a component and its host count as the same item. - Asking for a recalculation: raise
LayoutControllerUpdatedEventwhenever a property changes the result or children come and go, and the container schedules a new layout. - Applying its own modifier:
UpdateSelfdoes this when the container is a root, which the radial controller doesn't need. - Stitching: a
ContextTypeofnullmeans a parent never stitches this controller into its tree and measures it instead.
Put together, these pieces are all a controller needs, so the radial one above works as a template for your own. Replace
the circle math in Recalculate with your own placement rules, keep the bookkeeping and the update event as they are,
and the container takes care of applying the result.
Inside flexbox
The first example gives the ring a fixed Size, so its parent never needs to measure it. Without one, the parent calls
the controller's Measure, and the radial controller answers with the size of the box its children fill. The row is
laid out with that size, and then the ring places its numbers inside the box it ends up with.
Measure gets a width and a height from the parent, each with a measure mode that says how to treat it. With
Undefined the value means nothing and the parent wants the natural size, with Exactly the tree must be that size,
and with AtMost it can be up to that size. Most controllers only work out their preferred size and pass it
to LayoutTool.MeasureNode, which picks the right answer for each mode.
The radial controller does exactly that, recalculating without constraints and handing the size it got to the helper:
public Vector2 Measure(
float width,
MeasureMode widthMode,
float height,
MeasureMode heightMode
) {
// With no constraints, Recalculate sizes the controller to its children
Recalculate(Vector2.zero);
return LayoutTool.MeasureNode(
preferredSize: _size,
width: width,
widthMode: widthMode,
height: height,
heightMode: heightMode
);
}Measure should only calculate and never apply anything, because the parent may call it several times with different
constraints before it settles, and the container recalculates with the final box afterwards anyway. A controller that
adapts to the space it's offered, like a grid that adds rows when its width is AtMost, reads the width and its mode
itself instead of only reporting a preferred size.
Try adding and removing rings in this row, where none of them has a size of its own:

new Layout {
FlexController = {
AlignItems = Align.Center,
Gap = 3.pt
},
Children = {
CreateBlock(),
new Background {
LayoutController = new RadialLayoutController { Radius = 8f },
Children = { ... }
},
CreateBlock()
}
}Each new ring is measured by the row as soon as it's inserted, so its box is just as big as its numbers need, and the blocks on either side move apart to make room. The flex parent doesn't know what a ring does with its children, and only sees a leaf with a measured size.
Flexbox inside
We've seen that a flex row used Measure to fit a ring in, and this works the same way in reverse. A custom
controller can hold flex layouts as children, each one a Yoga tree of its own, and it measures them like it measures a
label. Calling Measure on a Layout asks its flex controller to calculate the subtree and report the size.
In this example the ring holds flex cards, a slider sets how many blocks fill the row inside each one, and a toggle wraps those rows:

new Background {
LayoutController = new RadialLayoutController { Radius = 12f },
Children = {
new Background {
FlexController = {
FlexDirection = FlexDirection.Column,
AlignItems = Align.Center,
Padding = 1.pt
},
Children = {
new Label { Text = "1" },
// A flex row of blocks, as many as the slider says
new Repeater<int, Background> {
FlexController = { FlexDirection = FlexDirection.Row },
sItems = blocks.Map(GetBlockIndices),
ConstructCell = _ => new Background {
FlexItem = { Size = 2.pt }
}
}
}
},
...
}
}Each card is a column that sizes itself to its number and its row of blocks. Move the slider or flip the toggle and every row changes shape, so the cards' trees change and the recalculation they schedule reaches the ring. The ring measures the cards again and centers each one on the same point of the circle. The card doesn't know it's on a ring, and the ring doesn't know the card is a column, since each side only measures and places.
Either direction costs an extra pass at the boundary, a measure followed by a recalculation with the final size. That's nothing for a ring of eight and noticeable for a tree of a thousand, so nest flex in flex where you can and keep separate trees for the places that need another algorithm.
Per-child properties
A controller often needs a setting for each child, like an offset that moves it away from its spot. Flexbox keeps those on the
child's modifier, the object behind FlexItem, and your controller can do the same with a class of its own. A modifier
implements ILayoutModifier, which only asks for a ModifierUpdatedEvent that you raise whenever a property changes.
The controller reads it from child.LayoutModifier while it recalculates, and falls back to a default when a child has
none or has another kind. Each item holds a single modifier, so a child of the ring uses yours instead of FlexItem.
When the event fires, the container calls UpdateChild and schedules a recalculation, so the controller doesn't have to
subscribe to anything.
This demo builds on the cards from the previous section, and a slider sets the Offset of cards 1, 3 and 4 through
a RadialModifier:

var offset = Remember(0f);
new Background {
LayoutController = new RadialLayoutController { Radius = 12f },
Children = {
new Background {
LayoutModifier = new RadialModifier {
sOffset = offset
},
Children = {
new Label { Text = "1" }
}
},
...
}
}Moving the slider up sends cards 1, 3 and 4 away from the center, and moving it down pulls them in, while cards 2 and 5 stay where they are. The grey box around the ring follows the cards, growing or shrinking so it always fits them. In the code, the controller only differs from the earlier one in the lines that read the modifier.