ClaFi Layout

Where everything ends up, and why

Introduction

A control in ClaFi is laid out by two things: what it says about itself, and what the control above it does with that answer. Everything in this manual is one of those two.

Both halves are stated where the control is built, as ordinary construction properties:

Stack& m_toolBar{ createTopBar<Stack>(
    Orientation::Horizontal,   // what this panel does with its children
    Padding{ 4.0f },             // what this panel says about itself
    Spacing{ 4.0f }
) };

There is no separate layout object to build, nothing to attach afterwards, and no pass to run by hand. A control that is added, hidden, resized or re-aligned asks the form for a pass, and the form runs one before the next paint.

This manual covers the layout. The other manuals cover what the controls are: Grid manual for grids, Writing Text and Text in Controls manuals for text, Events manual for handlers, Colour manual for how a control is painted.

Quick Start

A window with a toolbar across the top, a scrolling body, and a strip at the foot. It is the shape most applications open with, and it is four controls.

class MainPanel : public Panel
{
    using Panel::Panel;
private:
    // Across the top, left to right.
    Stack& m_toolBar{ createTopBar<Stack>(
        Orientation::Horizontal,
        Padding{ 4.0f },
        Spacing{ 4.0f }
    ) };
    Button& m_open{ m_toolBar.add<Button>(L"Open") };
    Button& m_save{ m_toolBar.add<Button>(L"Save") };
    // Takes whatever the strip has over, so Close sits at the right-hand end.
    FlexSpacer& m_gap{ m_toolBar.add<FlexSpacer>() };
    Button& m_close{ m_toolBar.add<Button>(L"Close") };

    // The middle. It fills whatever the toolbar and the strip leave.
    ScrollBoxWith<TextBox>& m_box{ createBody<ScrollBoxWith<TextBox>>(
        HostProps{ ScrollBars::Vertical },
        BodyProps{ Padding{ 8.0f } }
    ) };

    // Across the foot, as tall as what stands in it.
    Stack& m_status{ createBottomBar<Stack>(
        Orientation::Horizontal,
        Padding{ 4.0f }
    ) };
};

Three rules account for everything that happens here, and they are the whole of the next section:

The Model

The two passes

A layout is two walks over the same tree, in opposite directions.

1 · The measure - calculate() Form root Panel Stack Button children first, then the control itself every control writes its own scaledSize() and its composedMinSize() nobody knows yet what room they will get 2 · The align - align() Form root Panel Stack Button each control is handed a position and a box, and hands its own children theirs

The measure runs bottom-up. Control::calculate walks the children first, then asks the control what its content came to - calculateContent - and writes the answer into scaledSize(). Nothing in the tree knows yet how much room it will be given. A stack of buttons measures the widest button; a panel measures its bars and its body; a label measures its text.

The align runs top-down. Control::align hands a control a position and a box, the control's alignContent cuts that box up among its children, and each child is aligned into its part. The root is aligned into the window.

The two answers meet in Control::align. A control handed more room than it measured either takes it or gives it back, and that is what the alignment decides. Nothing else in the framework makes that choice.

A control's scaledWidth() and scaledHeight() hold the measure's answer between the two passes and the align's answer after them. Reading either from outside a pass is reading the last completed layout, which is normally what you want. Reading one while a pass is running - from inside a handler the pass raised - may be reading the half of it that has not been redone yet.

Asking for a pass

Nothing lays out on the spot. Every change asks the form for a pass, and the form runs a pass before the next paint however many things asked:

CallWhat it asks for
control.invalidate()a repaint of that control, no layout
control.invalidateFormAlign()a layout pass, then a paint
form.invalidateAlign()the same, said to the form
control.setVisible(...)a layout pass - hiding is a layout change
setHorizontalAlign, setVerticalAlign, setOrientation, setItemSizinga layout pass, when the value actually changes
control.scrollIntoViewOnAlign()a reveal measured against the layout the next pass produces
event.invalidatePass()from inside a pass: run the tree again before this frame goes out
invalidateAlign() is the route from outside a pass, and it is dropped when it is raised inside one: the form marks itself aligned before the walk begins. Inside a pass the channel is the event - a control that finds while being laid out that it was measured against the wrong width says event.invalidatePass(), and the form walks the tree again before the frame goes out. The two cannot be confused, because invalidatePass() is reachable only where an AlignEvent is. It must converge - see a layout that never settles.

A frame therefore shows a settled layout rather than the first attempt at one. The number of passes a single frame may spend is bounded: past it the frame goes out on what the last pass produced and the next one carries on, so a request that cannot converge costs frames rather than the application.

The box of a control

Control is the layout node: a box, a place in the tree, and the events that reach it. RichControl adds the appearance a control stores - metrics, colours, text - and is what nearly everything is built on. The properties below are its.

child child padding.x spacing.x padding.y border and radius are painted here and never measured minSize / preferredSize / maxSize hold the whole box, padding included

A button is where the two read most clearly. It draws up to three parts in a row - an indicator, an icon, its text - and lays them out itself rather than holding them as children: Padding is the inset from its edge to that run, Spacing is the gap between each part and the next.

icon its text padding.x spacing.x padding.y the same two properties a container spends on its children

The measure adds them in one order and it is worth knowing which: the content is measured, ceiled to whole pixels, grown by the padding on both sides, then raised to minSize and held down to maxSize.

Padding is what separates this control's edge from its content. There is no margin property: room between two controls is the parent's Spacing, and room in one particular place is a Spacer. Two ways to say one thing would be two ways for them to disagree.

Alignment

Two properties, four values each, and they decide one question: what the control does with a box larger than the size it measured.

ValueWhat the control comes out as
HorizontalAlign::Fill / VerticalAlign::Fillthe box it was handed. This is the default on both axes.
Left / Topthe size it measured, at the near edge of the box
Centerthe size it measured, in the middle of the box
Right / Bottomthe size it measured, at the far edge
the slot its parent gave it the control Fill measured Left measured Center measured Right
Label& m_title{ m_stack.add<Label>(
    Text{ TextAlign::Center, L"Live preview" },
    HorizontalAlign::Center,   // the LABEL sits in the middle of its slot
    VerticalAlign::Center
) };
HorizontalAlign places the control inside the room its parent gave it. TextAlign and HorizontalTextAnchor place the text inside the control. A label stretched across a row and a label sitting in the middle of one are different arrangements, and a centred text in a stretched label is a third.

The surplus is handed back

This is the rule to keep in mind whenever something does not move where it was expected to. A control that is not aligned Fill on an axis is given back the room it did not ask for: it comes out at the size it measured, and the rest of the slot stays empty. That is what Left, Center and Right mean, and it is also why two things that look like size statements behave differently:

Fill

  • The control is the width of its slot.
  • Its own children are laid out in that width.
  • A wrapping panel inside it wraps at that width.

Left, Center, Right

  • The control is the width its content came to.
  • Everything inside it is inside that measurement.
  • A wrapping panel inside it wraps at its own natural width, whatever the window is.
Fill and MaxSize on the same axis is a caller error. Fill says take the whole slot and a maximum says never be this big. Fill is what stands, and the measure still clamps to the maximum - so the control carries a content measured against its cap inside a box that ignores the cap. State one or the other.

What a control can be cut to

Beside the size it came to, the measure writes a second number: composedMinSize() - the least the control may be made. It is composed the same way the size is, out of the MinSize this control was given and the floor its content built from its children's.

A measurement never raises it. A control is as wide as it is because of what it holds today, and cutting it back is what leaves a strip of tabs or a run of columns the room to fit into a narrower box. So the floor is a MinSize somebody set, here or below, and a tree nobody set one in can be cut to nothing.

What reads it is whoever divides space and has less of it than was asked for - today, the window placement, which cuts a window to the room its side of the screen has and no further than this.

A ScrollBox states that its scrolled body gives way entirely: a box that scrolls can be made as small as the parts of it that do not scroll and still show everything it holds, because what falls past the edge is reached by scrolling. That is how a menu says it would rather be short than stand over the button it dropped from - nobody asks it, it states it.

Design units and scale

Every number written into a layout property is in design units. The framework scales them into device pixels per form, from the monitor the form was placed on and the application's own zoom, and it rounds each one to a whole pixel as it does so.

Padding{ 8.0f }   // 8 units - 8 pixels at 100%, 10 at 125%, 20 at 200%

That is the whole of it. The way to get a layout that scales is to not think about scaling at all. State the sizes the design should have and every form draws them at whatever the screen it landed on asks for. Dragging a window to another monitor, or changing the application's zoom while it is open, lays the form out again at the new factor with nothing in the application taking part.

Scaling something by hand is how that stops working, and it does not fail loudly - a number scaled at one factor and read back at another leaves a control at a fraction of the size it should have, and a form taken to 250% and back keeps it there. There is nothing for an application to scale.

Stroke thickness

Every line the framework draws - a control's border, a divider, the line along the open tab, an icon's outline - is stated as a Thickness grade rather than as a length. The grade decides the width in device pixels.

ThicknessDesign widthDrawn for
None0no stroke. The default of every metric block.
Hairline0.51a check mark's ring, the rectangle a selection is dragged out with
Thin0.83control and window borders, dividers, the tab line, icon outlines, the caret
Regular1.5the border of a control holding the focus
Bold2.0a tab line that carries the window's main navigation
Heavy4.0the tab indicator, a divider that breaks a strip into sections

A grade is drawn in whole pixels and never under one. From Thin up, each grade is at least one pixel wider than the grade below it, so no two of them draw alike at any scale. Hairline and Thin can both land on one pixel, which is the least a line can be.

ScaleHairlineThinRegularBoldHeavy
100%11234
150%11236
250%124510

Three properties state a grade: Border{ Thickness::Thin } on any control, a bare Thickness on a Divider, and TabLineThickness on a TabbedBox or a TabStrip. A paint handler asks the event for the pixel width, and a size stated in design units asks the free function for the design width:

const float line = event.scaledStrokeWidth(Thickness::Thin);  // device pixels, rounded as above
const float inset = strokeWidth(Thickness::Thin);         // design units - a padding one border wide
const float ring = event.borderWidth();                         // this control's own border in this frame

borderWidth() is the control's own grade, grown toward Regular while it holds the focus. The growth is the one width that passes through fractions of a pixel.

A stroke written as a float and scaled by hand takes no part in the rounding. Two such lines at different weights can come out the same width, and a focus ring built on one does not grow.

Sizing a Control

The metric properties

All of these are construction properties on any control built on RichControl. Each has a setter of the same name for changing it later.

PropertyMeansSetter
Padding{ x, y }room kept inside the control, around its contentsetPadding
Spacing{ x, y }room kept between the parts the control lays outsetSpacing
MinSize{ x, y }the smallest the control may be madesetMinSize
MaxSize{ x, y }the largest it may grow tosetMaxSize
PreferredSize{ x, y }what it asks for when it is free to choosesetPreferredSize, setPreferredWidth, setPreferredHeight
FixedSize{ x, y }writes MinSize and MaxSize to one value - no size of its ownsetFixedSize, setFixedWidth, setFixedHeight
Border{ Thickness }how heavy the control's border is, as a grade - no effect on layoutsetBorder
Radius{ r }its corner radius - no effect on layoutsetRadius
ControlMetricsthe whole block at once, from the themesetMetrics

A single float fills both axes, so Padding{ 8.0f } is Padding{ 8.0f, 8.0f }. A themed block is passed as itself, and a single metric stated beside it wins whichever order the two are written in - the block is read first and the separate properties are laid over it. Two properties of the same type in one pack are settled by order, and the last one wins:

Panel& m_card{ m_stack.add<Panel>(
    themeMetrics().page,      // the whole metric block the theme designs for a page
    UiElement::Section,       // the theme element it wears - see the Colour manual
    Padding{ 12.0f }          // and this padding instead of the block's
) };
A few controls are built on Control directly rather than on RichControl, because they store no appearance of their own, and none of them takes the metric block. Spacer and FlexSpacer take no properties at all - their size is a constructor argument. Divider takes properties, and its own Padding means something different there: it shortens the drawn line without moving what stands either side of it. Its weight is a bare Thickness, Thin unless one is stated.

Which size to state

MinSize, PreferredSize and MaxSize are three different statements and none of them says what another does.

Reach for

  • MinSize when the control must not lose what it shows. It is also the floor that survives a window being cut down.
  • MaxSize when the control must stop growing - a text column that must stay readable, a dropdown that must not cover the screen.
  • PreferredSize for "this size, unless something with a better claim decides otherwise" - a dialog that opens at a stated size and is still resizable, a pane that starts at a third of a split.
  • FixedSize for something whose size is not a question at all: a swatch, an icon well, a fixed-height strip.

Do not reach for

  • MinSize to make something a size. It is a floor, and a control with more content grows past it.
  • MaxSize together with Fill on the same axis.
  • PreferredSize where Fill would do. A preference is what a control would like; Fill is what its parent gives it.
  • A stated size to fix a layout that came out wrong. Find which control is not aligned the way it needs to be - that is nearly always the real answer.

PreferredSize is read per axis: zero on an axis means the content decides that axis, which is the usual case. A stated one stands in place of what the content measured - the content is still measured, and the children are still laid out - and it is held between minSize and maxSize like any other answer.

FixedSize stores nothing of its own. It writes MinSize and MaxSize to the same value, and setFixedWidth and setFixedHeight do the same on one axis. It is applied after both of them whichever order the properties are written in, so a fixed size beside a maximum wins.

Pinning the two together has one more consequence worth knowing: a control whose minimum and maximum agree has its own content measured by neither pass, so neither of them shapes its text. Its children are still laid out. For a list of a hundred thousand fixed-size items that is the difference between a layout and a stall.

Spacer and FlexSpacer

Two controls that draw nothing and take room.

// A gap of a stated size.
m_strip.add<Spacer>(12.0f);              // 12 units both ways
m_strip.add<Spacer>(12.0f, 4.0f);        // x, y

// The room nobody else claimed.
m_strip.add<FlexSpacer>();              // takes what its lane has over
m_strip.add<FlexSpacer>(8.0f);          // and never less than 8

Spacer keeps the size it was given. FlexSpacer takes whatever the lane has left over, split evenly where there is more than one of them. So one puts everything after it at the far end, two either side of an item centre that item, and two at one end share what is left.

Spacer Open Save Close left over FlexSpacer Open Save takes the slack Close the lane, as wide as the stack was granted
A lane only has something left over when the stack was given more room than it measured. A FlexSpacer in a stack nothing stretched has nothing to take, and what you get is the minimum size it was built with. If a flex spacer appears to do nothing, the question is not about the spacer - it is whether its stack fills the room it stands in. See a FlexSpacer that does nothing.
The same idea exists one level down, inside text: a menu item's shortcut key rides on a flex space in its own line, which is what lands every key on the same edge. See the TextEngine manual.

Arranging Children

Four containers account for nearly every arrangement. Each answers one question:

ContainerArranges byReach for it when
Stacklanes - a row, a column, or a wrap of eitherthings follow one another in order
Panelfive named slots around a bodysomething frames something else
ScrollBoxa viewport over a body laid out wholethe content outruns the room
Gridcolumns shared by every rowvalues line up across rows

Stack

A container that places its items in lanes. Three properties say how.

Stack& m_column{ createBody<Stack>(
    Orientation::Vertical,
    Padding{ 12.0f },
    Spacing{ 8.0f }
) };
Button& m_first{ m_column.add<Button>(L"First") };
PropertyValuesDefault
OrientationHorizontal, Vertical, HorizontalWrap, VerticalWrapVerticalWrap
LaneSize{ n }how many items one lane takes before the next beginsno limit
ItemSizingNatural, EqualNatural

Horizontal and Vertical are one lane. Everything goes in it, however long it gets. HorizontalWrap lays rows and breaks into a new one; VerticalWrap lays columns and breaks into a new one.

What a lane hands each item. Along the lane, the size the item measured. Across the lane, the whole thickness of the lane - which is as thick as the largest item in it. What the item does with the box it is given is its own alignment's answer, as always: a row of buttons aligned Fill are all as tall as the tallest, and the same buttons aligned VerticalAlign::Center keep their own heights and sit in the middle of the row.

What is left over. A stack given more room than it measured shares it out, and the sharing has two steps:

ItemSizing::Equal is the other way to divide a lane: every item is handed the same share, whatever it measured, and the lane is as long as the largest item times the count. A row of answers reads as one band that way rather than as buttons of three widths. A share is a box like any other, so the items in it need HorizontalAlign::Fill to actually be the same width - one aligned to an edge keeps its own size and the row goes ragged inside a set of equal shares.

Natural Save Discard Cancel each item the width it measured Equal Save Discard Cancel three shares, each as wide as the largest item Equal, items not Fill Save Discard Cancel the shares are still equal, what stands in them is not
// Save, Discard and Cancel as one band, each as wide as the widest.
Stack& m_answers{ createBottomBar<Stack>(
    Orientation::Horizontal,
    ItemSizing::Equal,
    Spacing{ 8.0f }
) };

Wrapping stacks

A wrapping stack reads everything through two words. Main is the way a lane runs, cross is the way the lanes stack. One routine serves both orientations, so whatever is true of rows is true of columns with the axes swapped.

main cross a lane is as thick as its largest item the surplus along the lane is divided per item, so the fullest lane ends on the edge a short last lane stays short

A lane breaks on two things: the LaneSize count, or the length running out. Whichever comes first, and never on the lane's own first item, which stands however long it is.

A stated LaneSize is a break, not a hint. Both passes read it, so a stack stated four to a lane stands four to a lane in a window of any size, and what a larger host gives it is room around the lane rather than a fifth item in it. Hidden items are passed over by both passes, so a lane holds the number it was measured to hold whatever has been hidden since.

The surplus across the lanes - whatever is left once every lane stands at its own thickness - goes to the lanes in equal parts. A list dropped into a window wider than its columns fills it, and a stack of rows told to fill a taller box spreads its rows down it the same way.

The surplus along the lanes is one number for the whole stack, and it is divided per item: what the length has over the longest lane, over the count of items on the fullest one. Every item then grows by the same amount, the fullest lane comes out exactly at the length, and a short last lane simply stays short rather than being stretched across the room for five.

The width a wrapping panel wraps at

This is the one place where the two passes have to be told about each other, and it is worth understanding because it explains the single most common layout surprise.

The measure runs bottom-up, before anyone knows what width the panel will be handed. So a wrapping panel measured on its own reports one lane holding everything, and the align breaks that into as many lanes as the width allows. The panel carries the width over: it remembers the content width it broke its rows at, the next measure breaks them at that same width, and where the width has changed it asks for one more pass. The pass after hands back the same number, which is what ends it.

Every host between it and the window must be aligned Fill. The remembered width is kept only where the panel's width really comes from outside, and a control that is not Fill keeps what it measured - so everything inside such a host is inside that measurement, and the wrapping panel under it wraps at its own natural width whatever the window is.
// A section whose body is a wrapping panel of tiles.
using ThemesGroup = ExpanderWith<Stack>;

ThemesGroup& m_group{ add<ThemesGroup>(
    HostProps{
        VerticalAlign::Top,          // the section follows the tiles as they wrap
        ExpanderViewMode::Divider,
        HeaderText{ L"User Themes" }
    },
    BodyProps{
        Orientation::HorizontalWrap,
        ItemSizing::Equal,
        Padding{ 12.0f, 4.0f },
        Spacing{ 4.0f }
    }
) };
Rows alone carry this. The width is the extent the measure runs against; a stack of columns takes no such maximum, so it has nothing to carry back and nothing to ask another pass about.

Panel and its five slots

A Panel holds bars around a body. The slots are named, each takes at most one control, and the slot decides where the control goes - not the order the controls were created in.

Top Bottom Left Right Body spans the content width as wide as it measured, as tall as the middle row whatever is left, and the one slot that can make the panel taller spans the content width
Stack&     m_toolBar{ createTopBar<Stack>(Orientation::Horizontal) };
StackView& m_tree{    createLeftBar<StackView>(MinSize{ 180.0f, 0.0f }) };
ScrollBox& m_view{    createBody<ScrollBox>() };
Stack&     m_status{  createBottomBar<Stack>(Orientation::Horizontal) };

The order the slots are settled in is fixed: top, bottom, left, right, body. A top or bottom bar spans the content width and is as tall as it measured. A left or right bar is as wide as it measured - it is the one place in a panel where a child's width is its own answer - and as tall as the middle row. The body gets what is left.

The panel's Spacing is what stands between a bar and the body. A bar that measured nothing takes its gap with it, so an empty or hidden bar leaves no trace.

A child that needs a constructor argument is still built where it is declared. The argument reaches it in the constructor body - m_title.text() << title; - rather than through the constructor's initializer list, where clang reports the member it is built on as not yet bound. See the FAQ.
A body may make its panel taller. If the body comes out taller than the slot it was offered, the panel grows to hold it - unless the panel's own height is dictated from outside, which is either VerticalAlign::Fill or the panel itself standing in somebody else's body slot. That is what lets a column of sections grow as sections are opened while the same column inside a window simply fills the window.

WithBody, HostProps and BodyProps

createBody<T> gives the slot a control but leaves body() answering a plain Control*. WithBody<Host, Body> puts a static type over the slot, so body() answers the type you named. Two aliases already exist for the common hosts:

ScrollBoxWith<OptionsPage>    // a scroll box whose body is an OptionsPage
ExpanderWith<Stack>           // a section whose body is a stack

Two controls are being built, so the properties arrive in two bags. Each bag routes by type, and a bag must not carry the same property type twice.

ScrollBoxWith<OptionsPage>& m_optionsBox{ createBody<ScrollBoxWith<OptionsPage>>(
    HostProps{                       // for the box
        ScrollBars::Auto,
        MaxSize{ k_maxFloat, k_optionsHeight }
    },
    BodyProps<>{}                    // for the page - nothing to say here
) };
OptionsPage& m_options{ m_optionsBox.body() };

ScrollBox

A ScrollBox is a PanelBase whose right and bottom bars are its scroll bars. Its body is laid out to the whole of its content and carried under the slot by the scroll position, which is why the body's own rect says where the content has reached rather than where it shows.

It is the only control in the framework that scrolls. Nothing else implements scrolling of its own, and anything at all can be put in one. That is why a view is written without a box - a grid, a text box, a list of tiles are each just as tall as their content - and whoever decides how much room it gets is who wraps it.
ScrollBoxWith<HexView>& m_box{ createBody<ScrollBoxWith<HexView>>(
    HostProps{ ScrollBars::Vertical },
    BodyProps{ Padding{ 4.0f } }
) };
ScrollBarsMeans
Bothboth bars stand whatever the content is. The default.
Vertical / Horizontalthat one bar
Noneno bars - the box still clips, and still scrolls to a control brought into view
Autoa bar on whichever axis the content overruns
Auto is answered during the measure, against the box's own maximum size. So it is for a box that measures itself: one under a MaxSize, or one that is an auto-fitting window's whole content. A box whose size comes from what contains it has no maximum of its own and never puts up an Auto bar - state the bars you want there.
the body, laid out to its whole content the slot the scroll position carries the body under the slot the slot holds still; the body travels the corner shows only while both bars are up

Two things follow from the body being laid out whole, and both are useful:

createCorner<T> fills the square the two bars leave between them. What is put there stands only while both bars are up.

Expander, tabs, pages

Expander - a panel whose body is shown and hidden by its header. The header owns the open state; the body is the panel's to show, so collapsing the header takes the whole body with it and the panel shrinks to the header. The header is held against the top of the view while what it heads is scrolled under it. ExpanderWith<T> types the body. ExpanderViewMode picks what the expander draws of itself: Section, Divider, Ellipsis or TreeNode.

PageControl - a stack showing one of its pages at a time. Pages are hidden as they are added and the current one is shown, and a hidden control is not measured - so under the default sizing a page nobody is looking at costs no layout at all. PageSizing says what the control answers for its own size with:

PageSizingMeans
CurrentPagethe page that shows, and nothing about the ones that do not. The default.
WidestPagethe largest of them all, both extents - so turning a page does not resize what holds it. The hidden pages are measured for this, once per page set and once per scale.

TabbedBox - a strip of tabs over a page control that follows the open one. TabsOrientation says which edge the strip runs along: HorizontalTop, HorizontalBottom, VerticalLeft, VerticalRight. TabLineThickness says how heavy the line along the open tab is - Thin unless stated, Bold for a strip that is the window's main navigation. TabViewMode says what the strip draws: Tab, the default, joins the open tab to the page with that line; ToolButton draws each tab as a tool button, set a tool bar's distance in from the edge, the open one shown selected, with no line and nothing joined to the page.

StackView - a Stack that holds several items selected at once. Every layout property of Stack reads on it unchanged; what it adds is selection, which is a different manual.

Divider - a line across whatever it stands in. It measures its Thickness both ways, so in a column it is a horizontal rule and in a row it is a vertical one. Its ends are rounded to half its width. A section break is Thickness::Heavy:

strip.add<Divider>(Thickness::Heavy, Padding{ 8.0f, 4.0f });

Content That Sizes Itself

Three things in a layout answer with a size of their own rather than taking one. They are where most of the remaining surprises live.

Text

A control's text is measured on the measure pass and, where it wraps, measured again on the align pass at the width it was actually granted. A narrower box breaks more lines out of a paragraph and a wider one joins them, so the height is not knowable until the width is.

PropertyValuesDefault
WordWrapYes, NoYes
HorizontalTextAnchorNone, Left, Center, RightNone
VerticalTextAnchorTop, Center, BottomTop

WordWrap::No is not only a drawing choice, it changes what the control measures. A text that does not wrap is measured against no width at all, so the control is as wide as its longest line whatever it is offered - which is exactly what a horizontal scroll bar needs to have a range to carry the view across. A text that wraps is measured against the box, and a box it cannot fit into shortens it rather than widening the control.

WordWrap::No as wide as its longest line, whatever the slot is the bar is what reaches the rest WordWrap::Yes broken to the slot, so a narrower box is a taller one
CodeBox& m_source{ m_box.createBody<CodeBox>(
    // Source is lines. The box is as wide as its longest one and the bar reaches it.
    WordWrap::No,
    Padding{ 6.0f }
) };
Left, Center and Right place the measured text block inside the control's own box, as a whole. TextAlign, written inside a Text or stated in the control's TextFormat, places the lines inside that block. None, the default, makes no block: the lines are aligned across the whole box. A single right-aligned line reaches the right edge under None; anchored Left, its block is the line itself, and it stays at the left. See Text Anchors.

One measuring rule is worth knowing because it reaches a level away: a button whose ButtonViewMode puts the picture above the words - TopLeftIcon and TopCenterIcon - measures its caption at the icon's width, so the caption wraps under the picture rather than making the button as wide as its name. In a wrapping stack under ItemSizing::Equal, where every share is as long as the largest item, one long caption measured on one line would size every share in the stack and can drop a whole column out of the grid. A caption stated WordWrap::No is measured on one line at its own width instead, so a word longer than the icon widens the button rather than being cut.

Grid columns

A grid is the one container whose arrangement is shared between children: every row is cut by the same columns, so a value in the third column lines up down the whole grid. Each column resolves its own width:

ColumnWidthModeMeans
FitContentwide enough for its widest cell across all rows. The default.
Fixedthe width stated, in design units
Fillcontent sets the floor, then the stated width divides the remainder as a ratio against the other Fill columns
room enough Name Level Notes Value FitContent Fixed Fill - takes the remainder FitContent narrowed narrowed Name Level Notes Value the widest gives way first the short ones are left alone no column goes below its own minimum

When the grid has less room than its columns asked for, the width is taken off the widest column first, then off the widest two together once they are level, and so on down - never below a column's own minimum. Narrowing a grid therefore eats the long columns and leaves the short ones alone.

The grid's own arrangement, its blueprint, its cells and their controls are the Grid manual's subject. What belongs here is the one thing a grid is from the outside: a control that measures to the sum of its columns, and that wants a ScrollBox around it whenever the rows can outrun the room.

The window

A form's root control is laid out into the window's geometry, and that is the whole of the connection. The root is an ordinary control in every other way - it has padding, it has a MinSize, it arranges its children like anything else.

AutoFit::No the content the window is the ceiling; the content is laid out into it, and cut to it where it does not fit AutoFit::Yes the content the content is measured first, and the window placed around it
// A main window that may be dragged down to this and no further.
constexpr MinSize k_mainFormMinSize{ 960.0f, 640.0f };

const std::unique_ptr<Form<ThemesBrowser>> form = this->createMainForm(k_mainFormMinSize);
return form->execute();
Under AutoFit::Yes the window is the content's own answer, all the way down. The remembered wrap width is not kept in such a form, because there is no outside width for a wrapping panel to be measured against - it would be bounding the very thing it is measured from. A wrapping panel in an auto-fitting window therefore stands at its LaneSize, which is the right answer: there is no width to fill.

Choosing an Arrangement

Most layout questions in ClaFi are settled by answering two questions in this order.

Stack one after another, in a lane Panel bars around a body ScrollBox a window onto something larger Grid every row cut by the same columns

First: what is the relationship between these controls?

If the controls...Reach for
follow one another in reading orderStack, Horizontal or Vertical
are a set that should flow to fill the roomStack, HorizontalWrap or VerticalWrap
frame something - a strip over a view, a bar beside itPanel, with the framed thing in the body
are one thing that may outrun its roomScrollBox around it
are values that must line up across rowsGrid
are one of several views of the same placePageControl, or TabbedBox with a strip
are a group the user should be able to put awayExpanderWith<Stack>

Then: for each control, who decides its size? That is the alignment, and it is one question per axis:

Nest rather than reach. A row of buttons where two are grouped is a Stack inside a Stack, not a special property on either. Nesting costs one control, and each level answers one question - which is what keeps a layout readable a year later.

Recipes

Commands at the far end of a strip

One FlexSpacer. Everything after it is pushed to the end of the lane.

Stack& m_toolBar{ createTopBar<Stack>(
    Orientation::Horizontal,
    Spacing{ 4.0f }
) };
Button& m_new{ m_toolBar.add<ToolButton>(L"New") };
Button& m_open{ m_toolBar.add<ToolButton>(L"Open") };
FlexSpacer& m_gap{ m_toolBar.add<FlexSpacer>() };
Button& m_settings{ m_toolBar.add<ToolButton>(L"Settings") };

This works because the strip is a top bar, which a panel hands the whole content width - so the strip has room over what its buttons measured, and the spacer is what asked for it.

One item in the middle

Two flex spacers, one either side. They share the surplus evenly, so what stands between them is centred whatever else is in the lane.

m_strip.add<FlexSpacer>();
Label& m_title{ m_strip.add<Label>(L"Untitled") };
m_strip.add<FlexSpacer>();

For a control that is alone in its slot, the alignment says it directly and no spacer is needed:

Label& m_empty{ createBody<Label>(
    L"Nothing selected",
    HorizontalAlign::Center,
    VerticalAlign::Center
) };

Not a flex spacer - a slot. A bottom bar is laid out against the bottom edge of the panel's content, and the body takes what is left between the two.

Stack& m_footer{ createBottomBar<Stack>(
    Orientation::Vertical,
    Padding{ 12.0f },
    Spacing{ 10.0f }
) };
Divider& m_rule{ m_footer.add<Divider>() };
CheckBox& m_keep{ m_footer.add<CheckBox>(L"Keep settings on this PC") };
Use a slot when the thing is structurally a band of the panel, and a flex spacer when it is one more item in a lane that happens to want the far end. A footer that must stay put while the body scrolls has to be a slot: a flex spacer inside a scrolled body scrolls with it.

A page that scrolls

The box is the page's, not the content's. A view that may be dropped into a window, into a dropdown or into a settings panel should not carry a box of its own - whoever places it knows how much room it gets.

ScrollBoxWith<OptionsPage>& m_optionsBox{ createBody<ScrollBoxWith<OptionsPage>>(
    HostProps{
        // A bar only where the options outrun this height, and none while they fit.
        ScrollBars::Auto,
        MaxSize{ k_maxFloat, k_optionsHeight }
    },
    BodyProps<>{}
) };
OptionsPage& m_options{ m_optionsBox.body() };

ScrollBars::Auto reads here because the box states a maximum height of its own. In a window, where the box simply fills whatever it is given, state ScrollBars::Vertical instead.

A wrapping grid of equal tiles

Three things together, and each of them is doing one job.

Stack& m_tiles{ createBody<Stack>(
    Orientation::HorizontalWrap,   // rows that break when the width runs out
    ItemSizing::Equal,             // every tile the width of the widest
    Padding{ 12.0f, 4.0f },
    Spacing{ 4.0f }
) };

And the tile itself states HorizontalAlign::Fill, so it takes the whole of the share it is handed. Without that the tiles are each as wide as their own caption and the tracks go ragged inside a set of equal shares. The tile states it ahead of its caller's properties, so a caller is free to say otherwise:

template<typename... Args>
ThemeTile::ThemeTile(const CreateParams& params, Args&&... args)
    :
    WithInPlaceEdit<Button>{
        params,
        HorizontalAlign::Fill,   // take the whole share the lane hands over
        std::forward<Args>(args)...
    }
{
}

Add LaneSize{ 4 } if the count is a design decision rather than a consequence of the width. It caps the lane in both passes, so the stack stands four to a lane in a window of any size and a wider host gives it room around the lane instead of a fifth tile.

Collapsible sections

A section is an Expander whose body is the column its options stand in, so collapsing the header takes the options with it.

using Section = ExpanderWith<Stack>;

Section& OptionsPage::addSection(const std::wstring_view caption)
{
    return add<Section>(
        HostProps{
            VerticalAlign::Top,
            ExpanderViewMode::Section,
            HeaderText{ InkGrade::Strong, caption }
        },
        BodyProps{
            Orientation::Vertical,
            k_groupPadding,
            Spacing{ k_captionSpacing }
        }
    );
}

VerticalAlign::Top on the section is what lets it grow. A section is a panel, and a panel grows to hold a body that came out taller than the slot it was offered - unless its own height is dictated from outside, which VerticalAlign::Fill is. The body here is a column of options that gets taller as a control inside it wraps, so the section has to be free to follow. See a panel that will not grow to its body.

Two panes side by side

A Panel with one pane in a side bar and the other in the body. The side bar is as wide as it measured, so it is the one that states a width; the body takes what is left.

ScrollBoxWith<StackView>& m_nav{ createLeftBar<ScrollBoxWith<StackView>>(
    HostProps{
        ScrollBars::Vertical,
        PreferredSize{ 220.0f, 0.0f },   // where it starts
        MinSize{ 120.0f, 0.0f }          // and the least it may be cut to
    },
    BodyProps<>{}
) };
ScrollBoxWith<CodeBox>& m_view{ createBody<ScrollBoxWith<CodeBox>>(
    HostProps{ ScrollBars::Both },
    BodyProps<>{}
) };

Zero on an axis of a metric says nothing about that axis, so MinSize{ 120.0f, 0.0f } states a width floor and leaves the height alone.

A window sized by its content

Popups do this by being popups. For a form of your own:

form.setAutoFit(AutoFit::Yes);

Every alignment then recomputes the window's bounds from the content rather than cutting the content to the window. State a MaxSize on the root, or on whatever inside it can grow without limit, or a long enough list will ask for a window taller than the screen.

Troubleshooting

Symptoms

A panel will not grow to hold its body

The symptom is content cut off at the foot of a section or a card - a wrapping panel showing one row of what it holds, a column of options with the last of them missing.

A panel grows to hold a body that came out taller than the slot it was offered, and a wrapping body is the usual way that happens: it re-wraps during the align and ends up taller than it measured. The panel refuses to grow when its own height is dictated from outside, and two things dictate it:

Give the panel VerticalAlign::Top. It is the host that needs it, not the wrapping panel inside - a Stack grows to its lanes whatever its own alignment says.

the section, VerticalAlign::Fill Section the body wrapped taller, the section did not follow its height was dictated from outside, so it kept it the section, VerticalAlign::Top Section it grows by what the body came out taller by
None of this reaches a body on a ScrollBox. A scrolled body is laid out to the whole of its content and its own alignment is overridden, so a wrapping panel dropped straight into a scroll box wraps and grows however it is aligned. If your wrapping panel is the thing being scrolled, this is not your problem.

A FlexSpacer does nothing

A flex spacer takes what its lane has over, and a lane that measured exactly what it was given has nothing over. Work outwards:

A control ignores its MaxSize

It is aligned Fill on that axis. Fill takes the slot and the maximum is not consulted. Pick one: MaxSize with an edge alignment, or Fill with no maximum.

A control set to MinSize comes out bigger

MinSize is a floor, not a size. A control with more content grows past it. FixedSize pins both bounds; PreferredSize states a size the content does not override.

Captions are cut off inside equal shares

Under ItemSizing::Equal, every share is as long as the largest item. An item that is not aligned Fill keeps its own measured size inside its share, so a set of them comes out ragged; an item whose content was measured at a different width than the share it was given is drawn clipped, because a width its text was never broken at is not a width its text fits. Align the items Fill.

Nothing moved after a change

The change did not ask for a pass. Property setters the framework owns ask for one; anything of your own that affects a size has to say invalidateFormAlign(). A repaint - invalidate() - is not a layout.

A scroll box will not put up a bar

ScrollBars::Auto is answered against the box's own maximum size. A box whose size comes from its parent has no maximum of its own and therefore no overrun to detect. State the bars you want, or give the box a MaxSize.

A layout that never settles

Something is calling invalidatePass() on a condition that never comes true. The condition must be one the next pass answers with the same number. Each frame spends a bounded number of passes, so the symptom is a layout that keeps changing under a still window rather than a hang.

Never ask for another pass by comparing what a control came to against what it measured. The measure ceils the content it answers and the align does not, so a value with a fraction in it disagrees by up to a whole unit for ever - every pass asks for another, and the program lays itself out until it is killed. Compare a width against the width the previous pass recorded, in design units: that converges in one step, because the pass measured against it hands back the same number.

A left or right bar is the wrong width

A left or right bar is as wide as it measured - it is the one place in a panel where a child's width is its own answer, and the panel will not stretch it. State the width on the bar: MinSize{ w, 0.0f } for a floor, PreferredSize{ w, 0.0f } for a starting width, FixedSize to pin it.

A control paints nothing and has no height

It measured nothing. A control with no text, no children and no stated size is zero by zero, and a control with no height paints nothing at all. Give it a MinSize, put something in it, or check that what should be in it is visible.

Do / Don't

Do

  • State properties where the control is built, in its member initializer.
  • Let a control be as big as what it holds, and say otherwise only where you mean it.
  • Give a panel VerticalAlign::Top where its body can come out taller than the slot it was offered.
  • Put the ScrollBox where the room is decided, not inside the view.
  • State a MinSize on anything that must survive a window being cut down.
  • Nest a second Stack rather than looking for a property.
  • Write every number in design units, and forget that scaling exists.

Don't

  • Fill and MaxSize on the same axis.
  • MinSize used as "the size it should be".
  • Scale anything by hand.
  • Lay out from inside a layout pass - ask for one.
  • Read scaledWidth() of something that has not been laid out yet and expect an answer. Use scrollIntoViewOnAlign() and OnAligned for work that needs a finished layout.
  • Give a scrolled body a maximum that comes from the box scrolling it. That is a ratchet - the framework states the one case that is safe, and it states it in design units for a reason.

Appendix · Quick reference

Properties a control takes

Every control built on RichControl, which is all but the few noted above.

PropertyTypeDefault
HorizontalAlignFill, Left, Center, RightFill
VerticalAlignFill, Top, Center, BottomFill
WordWrapYes, NoYes
HorizontalTextAnchorNone, Left, Center, RightNone
VerticalTextAnchorTop, Center, BottomTop
TextFormatTextFormat{ ... } - what the control's text is laid out over; several compose kind by kind. See the Text in Controls manualnone
Padding, Spacing{ x, y } or one floatthe theme block's, or zero
MinSize{ x, y }zero
MaxSize{ x, y }no limit
PreferredSize{ x, y }, zero means "content decides"zero
FixedSize{ x, y } - sets min and max togetherunset
Border{ Thickness }None, or the theme block's where one was passed
Radius{ value }zero, or the theme block's where one was passed
ControlMetricsthe whole block, from themeMetrics()zero on every field but maxSize

Container properties

ContainerPropertyValues
StackOrientationHorizontal, Vertical, HorizontalWrap, VerticalWrap
LaneSize{ n } - items per lane
ItemSizingNatural, Equal
ScrollBoxScrollBarsBoth, Vertical, Horizontal, None, Auto
PageControlPageSizingCurrentPage, WidestPage
TabbedBoxTabsOrientationHorizontalTop, HorizontalBottom, VerticalLeft, VerticalRight
TabLineThickness{ Thickness } - Thin unless stated
TabViewModeTab, ToolButton
ExpanderExpanderViewModeSection, Divider, Ellipsis, TreeNode
PanelBaseTextPlacementBody, Top, Left
a grid columnColumnWidthModeFitContent, Fixed, Fill

Building the tree

CallAnswers
container.add<T>(props...)T& - one more child, at the end
container.moveControl(child, index)puts a child at a place in the order
panel.createTopBar<T>(props...)T& in the Top slot
panel.createLeftBar<T>, createRightBar<T>, createBottomBar<T>the other bars
panel.createBody<T>(props...)T& in the Body slot
scrollBox.createCorner<T>(props...)the square between the two bars
WithBody<Host, Body>, ScrollBoxWith<T>, ExpanderWith<T>a typed body(), built from BodyProps
Controls are never constructed directly. Every control is built by the add- or create- call of the control that will own it, which is what gives it its parent and its form.

Reading a layout

MemberAnswers
scaledWidth(), scaledHeight(), scaledSize()the size the last pass gave it
left(), top(), topLeft(), boundsInParent()where it sits in its parent's content space
boundsInForm()the same, in form coordinates
visibleRectInForm()what of it lies inside every viewport above it
composedMinSize()the least it may be made
isHostedAsBody()whether its size is its host's to give
isWidthGivenByParent()whether its width is decided above it or by what it holds
scrollIntoView(), scrollIntoViewOnAlign()bring it into view now, or once the pass it is waiting for has run
form().onAligned(...)the form has been laid out and nothing asked for another pass
All manuals Top