Skip to content

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:

Key concepts:

  1. 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.
  2. Expanding Views - Children can expand to fill available space of their parent:

  3. Spacing - Configurable gaps between views:

  4. 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
  5. Layout Direction - Overall flow direction:

    • Horizontal layouts flow left-to-right
    • Vertical layouts flow top-to-bottom
    • Grid layouts use both directions
  6. Relativeness - children position is relative to parent's position, not an absolute position within a window.

  7. 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.

  8. Visibility - a layout manager (or any container that measures/positions its children by hand, i.e. in onIntrinsicMeasure, onComputeIntrinsicMinMaxAxis, onLayout) MUST check view->getVisibility() & Visibility::FLAG_CONSUME_SPACE before taking a child into account, see Visibility and space consumption.

  9. Margins in hand-written containers - AView::measure() and AView::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:

for (const auto& child : getViews()) {
    if (!(child->getVisibility() & Visibility::FLAG_CONSUME_SPACE)) {
        continue; // GONE: does not exist as far as layout is concerned
    }
    // ... measure / position child ...
}

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;
Everything else (padding, margins, fixed/min/max size, caching) is handled by the framework.


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:

  1. Something triggers AView::requestLayout() (resize, style change, content change).
  2. On the next render frame, AViewContainer::layout() is invoked on the root container.
  3. The layout manager calls child->computeMinMaxAxis(height) for each child to determine how to share available space among children.
  4. Once widths are decided, the layout manager calls child->measure(constraints) to get the final preferred size for each child under the resolved constraints.
  5. The layout manager calls child->layout(x, y, w, h) which invokes child->onLayout({w, h}).
  6. Results of measure() are cached in mMeasureCache keyed by AConstraints; the cache is invalidated by requestLayout().

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
setContents(
  Horizontal {
    _new<AButton>("1"),
    _new<AButton>("2"),
    _new<AButton>("3"),
  }
);

Vertical layout:

Code Result
setContents(
  Vertical {
    _new<AButton>("1"),
    _new<AButton>("2"),
    _new<AButton>("3"),
  }
);

Since container can be child of other container, we can create complex UIs using basic layout managers:

Code Result
setContents(
  Vertical {
    _new<AButton>("Up"),
    Horizontal {
        _new<AButton>("Left"),
        _new<AButton>("Right"),
    },
    _new<AButton>("Down"),
  }
);

Stacked layout:

Code Result
setContents(
  Stacked {
    _new<AView>() AUI_OVERRIDE_STYLE { BackgroundSolid(0xff0000_rgb), Expanding() },
    Label { "Test" },
  }
);

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
setContents(
  Vertical {
    _new<AButton>("Up"),
    Horizontal {
      _new<AButton>("Left"),
      _new<AButton>("Center"),
      _new<AButton>("Right") AUI_LET { it->setExpanding(); },
           // alias to it->setExpanding(2) ^^^^^^
    },
    _new<AButton>("Down"),
  } AUI_OVERRIDE_STYLE { MinSize { 300_dp, {} } },
);

Expanding views push remaining views in their container:

Code Result
setContents(
  Vertical {
    _new<AButton>("Up"),
    Horizontal {
        _new<AButton>("Left"),
        _new<AButton>("Center") AUI_LET { it->setExpanding(); },
        _new<AButton>("Right"),
    },
    _new<AButton>("Down"),
  } AUI_OVERRIDE_STYLE { MinSize { 300_dp, {} } },
);

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 old applyGeometryToChildrenIfNecessary(). Skips work if position, size and mWantsLayoutUpdate flag are all unchanged; otherwise sets position/size and calls AView::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