Layout Managers#
Layout manager is an object that manages placement and size of views inside containers.
Detailed Description#
In AUI, layout building consists of layout managers. Layout manager determines position and size of container's children views. A container is a view that consists of other views, called children. In general, layout manager does not allow going beyond the border of the container. A container can be a child of an another container. Such nested combinations allows to replicate nearly any UI layout.
This approach can be found in many popular declarative UI frameworks.
Design inspiration
The measurement pipeline (constraint propagation, intrinsic measure, inline/block axis terminology, per-constraints
caching) is heavily inspired by Chromium's LayoutNG — the layout
engine powering Chrome/Edge since ~2019. LayoutNG introduced the idea of passing an immutable
ConstraintSpace into each layout node and getting back an immutable fragment, which maps directly to AUI's
AConstraints → measure() → layout() pipeline.
The ALayout is the base class for all layout managers in AUI. Layout managers are responsible for:
- Positioning child views within their container
- Calculating minimum sizes
- Handling view additions and removals
- Managing spacing between views
- Respecting view margins and alignment
- Supporting expanding/stretching of views
Note
You can use AUI Devtools to play around with layouts, especially with Expanding property, to get better understanding on how does layout work in AUI.
Common layout managers include:
- AHorizontalLayout - Arranges views in a horizontal row
- AVerticalLayout - Arranges views in a vertical column
- AStackedLayout - Centers views, displaying them on top of each other
- AAdvancedGridLayout - Arranges views in a grid with customizable cell sizing
Key concepts:
-
Minimum Size - Layout managers calculate minimum size requirements by:
- Considering minimum sizes of child views
- Adding margins and spacing
- Respecting fixed size constraints
- Following AUI Box Model.
-
Expanding Views - Children can expand to fill available space of their parent:
- Set via
AView::setExpanding()orass::Expandingon a child - Requires parent to have
ass::FixedSizeorass::MinSizeor Expanding set to take effect - Independent for horizontal/vertical directions
- Ignored if
ass::FixedSizeis set
- Set via
-
Spacing - Configurable gaps between views:
- Set via
ALayout::setSpacing()orass::LayoutSpacingof the parent view - Part of minimum size calculations of the parent view
- Applied uniformly between its child views
- Set via
-
Margins - Space around individual views:
- Set per-view via ASS or margins property
- Respected during layout
- Part of minimum size calculations of the parent view
-
Layout Direction - Overall flow direction:
- Horizontal layouts flow left-to-right
- Vertical layouts flow top-to-bottom
- Grid layouts use both directions
-
Relativeness - children position is relative to parent's position, not an absolute position within a window.
-
Nesting - you can nest containers into containers, and so on. When we say "container", it means a AViewContainer. When we say "Vertical", we imply a AViewContainer with AVerticalLayout as the layout manager.
-
Visibility - a layout manager (or any container that measures/positions its children by hand, i.e. in
onIntrinsicMeasure,onComputeIntrinsicMinMaxAxis,onLayout) MUST checkview->getVisibility() & Visibility::FLAG_CONSUME_SPACEbefore taking a child into account, see Visibility and space consumption. -
Margins in hand-written containers -
AView::measure()andAView::layout()work with the view without its margin; a container that doesn't use a layout manager must add/subtract margins itself, see Margins and manual layout.
Margins and manual layout#
AView::measure() returns the size of the view without its margin, and AView::layout(pos, size) expects the
position and size of the view without its margin as well. Margin is the parent's business: a container that
measures or positions its children by hand (i.e. AScrollArea, AScrollbar) must handle it itself:
- Measuring: add the child's margin to the measured size:
child->measure(...).x + margin.horizontal(),child->measure(...).y + margin.vertical(). - Constraints: when passing the available space of one axis to the child, subtract the child's margin on that
axis first, i.e.
AConstraints::fixedBlock(max(0, availableHeight - margin.vertical())). - Positioning: shift the position by the leading margin:
layout(slotPos + {margin.left, margin.top}, ...). - Sizing: subtract the margin from the slot size you give to the child:
slotSize - {margin.horizontal(), margin.vertical()}. - Never negative: the slot can be smaller than the margin; clamp the result with
glm::max(0, ...).
const auto margin = child->getMargin();
child->layout(
slotPos + glm::ivec2(margin.left, margin.top),
glm::max(glm::ivec2(0), slotSize - margin.occupiedSize()));
Warning
Forgetting margins in a hand-written container leads to children that overlap their neighbours or ignore
ass::Margin altogether. Layout managers (AHorizontalLayout, AVerticalLayout, ...) do it for you.
Visibility and space consumption#
Whether a child occupies space in its parent is determined by the Visibility::FLAG_CONSUME_SPACE flag of
AView::getVisibility():
| Visibility | FLAG_CONSUME_SPACE |
Occupies space |
|---|---|---|
VISIBLE, INVISIBLE, UNREACHABLE, CONSUME_SPACE_ONLY |
set | yes |
GONE |
not set | no |
A child without FLAG_CONSUME_SPACE (Visibility::GONE) must be treated as if it did not exist: it contributes
neither its size, nor its margins, nor spacing to the parent. Note that AView::measure() and
AView::computeMinMaxAxis() do not check visibility on their own and happily return the size of a GONE view
(i.e. its ass::FixedSize). Checking is the caller's responsibility.
Warning
Forgetting this check is a classic source of "phantom" sizes: a hidden child with ass::FixedSize or
ass::MinSize silently inflates its parent. Example: a scrollbar that took the height of its GONE arrow
buttons and became 19px thick instead of 7px. When you iterate over children (or over a fixed set of child
views you own) in any measuring or layout code, filter them:
Terminology#
The layout system uses a handful of specific terms. Here is what they mean:
Measure — the process of asking a view "how much space do you want?" without committing to a final position.
Analogous to CSS getComputedStyle / Flutter layout dry-run / Android measure. The result is a preferred size
under given constraints, cached until requestLayout() is called.
Layout — the process of telling a view "here is your final position and size". Called after measure. Analogous
to CSS layout / Flutter layout / Android layout.
Intrinsic — means "content only, before the framework applies its wrapper logic". When you override
onIntrinsicMeasure, you only deal with the raw content size — padding, margin, ass::FixedSize, ass::MinSize,
ass::MaxSize are not your concern; the public measure() adds them on top automatically. Think of it as
"what size would this view naturally want if it had no external constraints imposed by the stylesheet?"
Constraints (AConstraints) — a box of four numbers passed into measure: minimum and maximum allowed width
(minInline / maxInline) and height (minBlock / maxBlock). A value of -1 means unlimited. Inspired by
Flutter's BoxConstraints and
Chromium LayoutNG's ConstraintSpace.
MinMaxAxis (AMinMaxAxis) — a pair {min, max} representing the range of widths a view can occupy for a
given height. Used by layout managers to decide how to share horizontal space before committing to constraints.
Inline axis / inline size — in a standard left-to-right horizontal layout this is simply the width (X). The name comes from CSS writing modes: "inline" is the direction text flows within a line. AUI uses this term instead of "width" so that the same layout code remains correct for future RTL or vertical-text writing modes, where "inline" would map to the opposite or perpendicular axis.
Block axis / block size — in a standard left-to-right horizontal layout this is simply the height (Y). "Block" is the direction in which blocks of text stack. Same rationale as above.
TL;DR for LTR layouts: inline = width, block = height.
Free space — container size minus the sum of children's measured minimum sizes. Distributed among children with non-zero Expanding values after the measure pass.
Implementing a custom AView: what to override#
If you are implementing a custom AView (say, MyView), here is what you need to know about the layout system.
There are three virtual methods you may override in AView:
| Method | Purpose | Override? |
|---|---|---|
onIntrinsicMeasure(AConstraints) |
Returns preferred content size under given constraints | Usually |
onComputeIntrinsicMinMaxAxis(int height) |
Returns min/max content width for a given height | Sometimes |
onLayout(glm::ivec2 size) |
Called when position/size are finalized; use to position children | Only for containers |
Containers must skip children without FLAG_CONSUME_SPACE
If your view measures or lays out child views (in onIntrinsicMeasure, onComputeIntrinsicMinMaxAxis or
onLayout), you must check child->getVisibility() & Visibility::FLAG_CONSUME_SPACE before taking the
child into account (size, margins, spacing). measure() and computeMinMaxAxis() of a GONE view return its
normal size, they do not return zero. See Visibility and space consumption.
What does 'intrinsic' mean?
Intrinsic = "what size does the content itself want, ignoring all external stylesheet rules?"
When you override onIntrinsicMeasure, you only return the raw content size. The framework's public
measure() and computeMinMaxAxis() then wrap your result and add padding, margin, ass::FixedSize,
ass::MinSize, ass::MaxSize on top. Never call the on* intrinsic methods directly — always go
through the public wrappers. See Terminology for the full glossary.
1. onIntrinsicMeasure – the primary override#
glm::ivec2 MyView::onIntrinsicMeasure(AConstraints constraints) override {
// constraints describe the *content* box available to this view.
// Return the preferred content {width, height}.
int w = computeMyPreferredWidth();
int h = computeMyPreferredHeight();
return {w, h};
}
AConstraints carries four fields:
| Field | Meaning |
|---|---|
minInline |
Minimum required width (usually 0) |
maxInline |
Maximum allowed width, or -1 if unlimited |
minBlock |
Minimum required height (usually 0) |
maxBlock |
Maximum allowed height, or -1 if unlimited |
Helper predicates: isUnlimitedInline(), isUnlimitedBlock(), isInlineTight(), isBlockTight().
Real example – AAbstractLabel:
glm::ivec2 AAbstractLabel::onIntrinsicMeasure(AConstraints constraints) {
int measuredWidth = getFontStyle().getWidth(getTransformedText());
if (mTextOverflow != ATextOverflow::NONE
&& !constraints.isUnlimitedInline()
&& measuredWidth > constraints.maxInline) {
measuredWidth = constraints.maxInline; // clamp and trigger ellipsis
}
return { measuredWidth, getFontStyle().getLineHeight() };
}
2. onComputeIntrinsicMinMaxAxis – width-for-height query#
AMinMaxAxis MyView::onComputeIntrinsicMinMaxAxis(int height) override {
// height is the content height, or -1 if unconstrained.
// Return {min content width, max content width}.
int w = computeMyContentWidth(height);
return { .min = w, .max = w };
}
This is called by layout managers that need to know the range of widths a view can occupy before they have
committed to a height (e.g., to determine how to share horizontal space). If your view has a fixed intrinsic
width regardless of height, just return {w, w}. If the view can stretch, return different min and max.
When to override: always override alongside onIntrinsicMeasure when your content width depends on
available height (e.g., word-wrapped text, aspect-ratio-locked images). For a simple fixed-content view
the default implementation (which delegates to onIntrinsicMeasure) is usually sufficient.
3. onLayout – finalising geometry (containers only)#
void MyContainer::onLayout(glm::ivec2 size) override {
// size is the final {width, height} of this container.
// Position and size each child explicitly:
for (auto& child : mChildren) {
glm::ivec2 childSize = child->measure(AConstraints{ .maxInline = size.x, .maxBlock = size.y });
child->layout(0, currentY, childSize.x, childSize.y);
currentY += childSize.y;
}
}
For most leaf views (labels, images, custom-drawn widgets) you do not need to override onLayout.
Summary for leaf view implementors#
If you only draw content and have no children, you need one thing:
glm::ivec2 onIntrinsicMeasure(AConstraints constraints) override;
Measurement pipeline in detail#
The layout system has three entry points, from simplest to most complex:
onComputeIntrinsicMinMaxAxis(height)
│
│ (override for content min/max width)
▼
computeMinMaxAxis(height) ← public; adds padding / FixedSize / MinSize / MaxSize
onIntrinsicMeasure(constraints)
│
│ (override for content preferred size)
▼
measure(constraints) ← public; adds padding / FixedSize / MinSize / MaxSize, cached
onLayout(size)
│
│ (override in containers to position children)
▼
layout(x, y, w, h) ← public; sets position & size, calls onLayout
Step-by-step during a layout pass:
- Something triggers
AView::requestLayout()(resize, style change, content change). - On the next render frame,
AViewContainer::layout()is invoked on the root container. - The layout manager calls
child->computeMinMaxAxis(height)for each child to determine how to share available space among children. - Once widths are decided, the layout manager calls
child->measure(constraints)to get the final preferred size for each child under the resolved constraints. - The layout manager calls
child->layout(x, y, w, h)which invokeschild->onLayout({w, h}). - Results of
measure()are cached inmMeasureCachekeyed byAConstraints; the cache is invalidated byrequestLayout().
Axis terminology#
AUI uses CSS-inspired axis names to stay writing-mode agnostic (for future RTL/vertical text support):
| AUI term | LTR horizontal (default) | RTL horizontal | Vertical (e.g. Japanese) |
|---|---|---|---|
| inline | horizontal ➡️ (X) | horizontal ⬅️ (X) | vertical ⬇️ (Y) |
| block | vertical ⬇️ (Y) | vertical ⬇️ (Y) | horizontal ➡️ (X) |
Relationship to Expanding / stretch factors#
Qt heritage & Flexbox roadmap
Expanding is inherited from Qt's QSizePolicy::expandingDirections() / QSizePolicy::HorizontalPolicy
model. In Qt, the stretch factor was the primary sizing mechanism. In AUI it has been demoted to a
secondary one: measure() / computeMinMaxAxis() run first and establish preferred sizes, then
Expanding distributes whatever space is left over. The API surface (setExpanding(), ass::Expanding,
SpacerExpanding) is intentionally kept familiar for Qt users.
Known limitation: Expanding only grows views beyond their measured minimum — it cannot shrink
them below it. CSS flex-shrink / flex-wrap / align-items are therefore not expressible with the
current mechanism.
Future plan: A dedicated AFlexLayout will implement the full CSS Flexbox algorithm on top of the
same measure() / computeMinMaxAxis() infrastructure. Expanding in AHorizontalLayout /
AVerticalLayout will remain as a backward-compatible convenience shorthand, analogous to Android's
LinearLayout layout_weight coexisting with ConstraintLayout. When AFlexLayout lands,
setExpanding(n) will map to flex-grow: n inside flex containers.
Expanding (stretch factor) is a secondary mechanism layered on top of measurement. After the layout manager
has measured all children via computeMinMaxAxis / measure, it distributes remaining free space among
children that have a non-zero expanding value, proportionally to that value. A child with Expanding(2) gets twice
as much free space as one with Expanding(1).
Free space = container size − sum of children's measured minimum sizes.
ass::FixedSize on a child makes the layout manager ignore that child's expanding value for the fixed axis.
Layout Examples#
Horizontal layout:
| Code | Result |
|---|---|
![]() |
Vertical layout:
| Code | Result |
|---|---|
![]() |
Since container can be child of other container, we can create complex UIs using basic layout managers:
| Code | Result |
|---|---|
![]() |
Stacked layout:
| Code | Result |
|---|---|
|
![]() |
Expanding#
Expanding is a stretch factor — a secondary, opt-in property that tells the layout manager how to distribute free space left over after all children have been measured.
Measure first, expand second
The layout pipeline always runs measure first: every child reports its preferred size via
onIntrinsicMeasure. Only after that does the layout manager look at Expanding to distribute whatever space
remains. Expanding never replaces measurement — it only stretches views beyond their measured minimum.
Note
You can use AUI Devtools to play around with layouts, especially with the Expanding property, to get better understanding of how layout works in AUI.
Expanding is set per-axis (one value for X, one for Y), though it is convenient to set both at once. Horizontal layouts ignore the Y expanding of their children; Vertical layouts ignore the X expanding.
Views are created without any expanding by default — they take exactly as much space as their measured size requires. When one or more children have a non-zero expanding value, the layout manager collects the free space (container size minus the sum of all children's measured minimum sizes) and distributes it proportionally to the expanding weights.
Expanding a child does not affect the parent's own size or expanding property. To make the container itself
grow, use AView::setExpanding() on the parent, or the Expanding variant of declarative container notation
(Vertical::Expanding, Horizontal::Expanding, Stacked::Expanding).
Free space of a container is its size minus the sum of its children's measured minimum sizes. Note that a
container normally shrinks to the minimum size required by its children. To give the container extra space to
distribute, set ass::FixedSize, ass::MinSize, or Expanding on the container itself.
You can use ass::Expanding ASS property, or AView::setExpanding method to specify Expanding:
| Code | Result |
|---|---|
![]() |
Expanding views push remaining views in their container:
| Code | Result |
|---|---|
![]() |
Expanding view does affect expanding environment inside a single container. If there's one view with expanding set to any positive value it would occupy all free space in the container. If there is a view with expanding equal to 1 and another view with expanding equal to 2 the first view would occupy one third of free space, the second view would occupy two thirds of free space:
Vertical {
_new<AButton>("Left") AUI_LET { it->setExpanding(1); },
_new<AButton>("Right") AUI_LET { it->setExpanding(2); }, // will be twice as big as "Left"
}
You can use ASpacerExpanding as blank expanding view:
Vertical {
_new<AButton>("Left"),
SpacerExpanding(),
_new<AButton>("Right"),
}
Note
FixedSize nullifies Expanding's action (on per axis basic).
Applying size#
- Size of each view in tree is calculated during this phase.
AView::redraw()- triggers surface redraw. Surface applies layout before rendering.AView::layout()- the single entry point for positioning and sizing a view. Replaces the oldapplyGeometryToChildrenIfNecessary(). Skips work if position, size andmWantsLayoutUpdateflag are all unchanged; otherwise sets position/size and callsAView::onLayout(glm::ivec2). Always called by the parent layout manager — do not call directly.
Layout boundaries#
AView::requestLayout() normally travels up the view hierarchy, up to the surface, which lays the whole hierarchy
out before the next frame. A view whose size is fixed on both axes occupies exactly the same space no matter what
happens inside it, so everything above it keeps the geometry it already has; such a view is a
layout boundary and the request stops there. Instead, the view enqueues itself in its
surface (@see ASurface::enqueueLayoutRoot) and the surface lays out that subtree alone before rendering.
The values the parent's layout does depend on — fixed size, min/max size, margin, expanding, visibility — are
changed through AView::markLayoutContributionChanged(), which lifts the boundary for one request so that the
parent finds out and lays out as usual.
Migration from master
applyGeometryToChildrenIfNecessary() no longer exists. The equivalent sequence on feat/layout-measure is:
call requestLayout() to invalidate caches, then let the normal render cycle drive layout() top-down.
If you need to force an immediate synchronous layout pass (e.g. in tests), call layout(x, y, w, h) directly
on the root container with its current geometry.
Size calculation#
See Measurement pipeline in detail for the full pipeline. In brief:
- AView::computeMinMaxAxis() — returns min/max inline (width) range for a given block (height). Wraps
AView::onComputeIntrinsicMinMaxAxis and adds padding, fixed/min/max constraints.
- AView::measure() — returns preferred outer size under AConstraints. Wraps AView::onIntrinsicMeasure
and adds padding, fixed/min/max constraints. Result is cached per-constraints set.
- After measured sizes of children are determined, the layout manager distributes remaining free space among
children that have a non-zero Expanding value, proportionally to that value.
Special cases#
- AScrollArea: requires special handling for viewport positioning and size compensation
- AForEachUI: manages view inflation/deflation based on visibility
- Performance Optimizations: Views outside viewport may be left unupdated to improve performance
Related Pages#
-
Places views in a stack (along z axis).
-
Places views in a row.
-
Places views in a column.
-
Grid layout. Unlike AGridLayout, cells may have different sizes.
-
Imitates behaviour of word wrapping, but uses views instead words
-
Grid layout with fixed-size cells.
-
Base class for all layout managers.
-
Absolute positioning layout. Allows to explicitly set your own coordinates.





