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.
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:
- A control is as big as what it holds, unless it states otherwise.
- A container hands each child a box. What the child does inside that box is the child's own alignment to say.
- Fill takes the box. Any other alignment keeps the size the control measured and stands somewhere in it.
The Model
The two passes
A layout is two walks over the same tree, in opposite directions.
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.
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:
| Call | What 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, setItemSizing | a 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.
Paddingis kept inside the control, around its content. Children and text both start at it.Spacingis kept between the parts a control lays out - its children, or the icon, indicator and text a button draws. One part spends none of it.BorderandRadiusare painted, never measured. The layout pass is not given them at all, so changing either moves nothing.MinSize,PreferredSize,MaxSizeandFixedSizeare the whole box, padding included, and not the content inside it.
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.
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.
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.
| Value | What the control comes out as |
|---|---|
HorizontalAlign::Fill / VerticalAlign::Fill | the box it was handed. This is the default on both axes. |
Left / Top | the size it measured, at the near edge of the box |
Center | the size it measured, in the middle of the box |
Right / Bottom | the size it measured, at the far edge |
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.
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.
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.
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.
Thickness | Design width | Drawn for |
|---|---|---|
None | 0 | no stroke. The default of every metric block. |
Hairline | 0.51 | a check mark's ring, the rectangle a selection is dragged out with |
Thin | 0.83 | control and window borders, dividers, the tab line, icon outlines, the caret |
Regular | 1.5 | the border of a control holding the focus |
Bold | 2.0 | a tab line that carries the window's main navigation |
Heavy | 4.0 | the 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.
| Scale | Hairline | Thin | Regular | Bold | Heavy |
|---|---|---|---|---|---|
| 100% | 1 | 1 | 2 | 3 | 4 |
| 150% | 1 | 1 | 2 | 3 | 6 |
| 250% | 1 | 2 | 4 | 5 | 10 |
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.
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.
| Property | Means | Setter |
|---|---|---|
Padding{ x, y } | room kept inside the control, around its content | setPadding |
Spacing{ x, y } | room kept between the parts the control lays out | setSpacing |
MinSize{ x, y } | the smallest the control may be made | setMinSize |
MaxSize{ x, y } | the largest it may grow to | setMaxSize |
PreferredSize{ x, y } | what it asks for when it is free to choose | setPreferredSize, setPreferredWidth, setPreferredHeight |
FixedSize{ x, y } | writes MinSize and MaxSize to one value - no size of its own | setFixedSize, setFixedWidth, setFixedHeight |
Border{ Thickness } | how heavy the control's border is, as a grade - no effect on layout | setBorder |
Radius{ r } | its corner radius - no effect on layout | setRadius |
ControlMetrics | the whole block at once, from the theme | setMetrics |
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
) };
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
MinSizewhen the control must not lose what it shows. It is also the floor that survives a window being cut down.MaxSizewhen the control must stop growing - a text column that must stay readable, a dropdown that must not cover the screen.PreferredSizefor "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.FixedSizefor something whose size is not a question at all: a swatch, an icon well, a fixed-height strip.
Do not reach for
MinSizeto make something a size. It is a floor, and a control with more content grows past it.MaxSizetogether with Fill on the same axis.PreferredSizewhere 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.
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.
Arranging Children
Four containers account for nearly every arrangement. Each answers one question:
| Container | Arranges by | Reach for it when |
|---|---|---|
Stack | lanes - a row, a column, or a wrap of either | things follow one another in order |
Panel | five named slots around a body | something frames something else |
ScrollBox | a viewport over a body laid out whole | the content outruns the room |
Grid | columns shared by every row | values 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") };
| Property | Values | Default |
|---|---|---|
Orientation | Horizontal, Vertical, HorizontalWrap, VerticalWrap | VerticalWrap |
LaneSize{ n } | how many items one lane takes before the next begins | no limit |
ItemSizing | Natural, Equal | Natural |
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:
- A flex spacer in the lane takes it, split evenly where there is more than one.
- Where nothing asked, a single lane leaves it at the far end and every item stands at the size it measured. A wrapping stack shares it among its items instead - see below - and only where the stack itself was told to fill that way.
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.
// 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.
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.
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.
// 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 }
}
) };
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.
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.
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.
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.
ScrollBoxWith<HexView>& m_box{ createBody<ScrollBoxWith<HexView>>(
HostProps{ ScrollBars::Vertical },
BodyProps{ Padding{ 4.0f } }
) };
ScrollBars | Means |
|---|---|
Both | both bars stand whatever the content is. The default. |
Vertical / Horizontal | that one bar |
None | no bars - the box still clips, and still scrolls to a control brought into view |
Auto | a 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.
Two things follow from the body being laid out whole, and both are useful:
- A scrolled body keeps what it measured. The align pass normally cuts a control to the slot it was given; a body on a scroll box is exempt, which is what gives the bars a range to carry the view across.
- A wrapping or word-wrapping body is broken at the viewport. The box states the viewport as that body's maximum width, so the body shapes its text once, at the width it will be drawn at, rather than at a width no line reaches.
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:
PageSizing | Means |
|---|---|
CurrentPage | the page that shows, and nothing about the ones that do not. The default. |
WidestPage | the 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.
| Property | Values | Default |
|---|---|---|
WordWrap | Yes, No | Yes |
HorizontalTextAnchor | None, Left, Center, Right | None |
VerticalTextAnchor | Top, Center, Bottom | Top |
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.
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:
ColumnWidthMode | Means |
|---|---|
FitContent | wide enough for its widest cell across all rows. The default. |
Fixed | the width stated, in design units |
Fill | content sets the floor, then the stated width divides the remainder as a ratio against the other Fill columns |
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.
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.
- The window is a ceiling, not a setting. What the root may grow to is the window it has.
- The floor is the root's
composedMinSize(). That is what a placement cuts a window down to and no further, and it is composed from theMinSizevalues set in the tree - never from what anything measured. AutoFit::Yesturns the relationship round. Every alignment of the form becomes a placement: the window's bounds are recomputed from the content instead of the content being cut to fit the window. It is what a menu, a tooltip and a dropdown are.
// 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();
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.
First: what is the relationship between these controls?
| If the controls... | Reach for |
|---|---|
| follow one another in reading order | Stack, Horizontal or Vertical |
| are a set that should flow to fill the room | Stack, HorizontalWrap or VerticalWrap |
| frame something - a strip over a view, a bar beside it | Panel, with the framed thing in the body |
| are one thing that may outrun its room | ScrollBox around it |
| are values that must line up across rows | Grid |
| are one of several views of the same place | PageControl, or TabbedBox with a strip |
| are a group the user should be able to put away | ExpanderWith<Stack> |
Then: for each control, who decides its size? That is the alignment, and it is one question per axis:
- It should be as big as the room it is given -
Fill, which is the default and usually right for the thing in a body slot. - It should be as big as what it holds -
Left,Top,Center,RightorBottom, and the one you pick says where it then sits. - It should be a stated size - a metric property, and then the alignment says where the surplus goes.
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
) };
A footer that stays at the foot
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") };
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:
VerticalAlign::Fill- a parent stretched the panel to the room it had. Fill is the default, so this is what you get by saying nothing.- The panel is itself somebody's body. Its host handed it exact dimensions and has already laid the rest of itself out around that height.
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.
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:
- Is the stack itself aligned Fill along the lane it runs? A stack aligned Left keeps the width it measured and there is no surplus to hand out.
- Is the stack in a slot that gives it room - a top or bottom bar, a body - or is it one item in another lane that only gave it its own measurement?
- Is the spacer inside the lane it is meant to stretch? A flex spacer in a nested stack stretches that stack, not the one above it.
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.
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::Topwhere its body can come out taller than the slot it was offered. - Put the
ScrollBoxwhere the room is decided, not inside the view. - State a
MinSizeon anything that must survive a window being cut down. - Nest a second
Stackrather than looking for a property. - Write every number in design units, and forget that scaling exists.
Don't
- Fill and
MaxSizeon the same axis. MinSizeused 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. UsescrollIntoViewOnAlign()andOnAlignedfor 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.
| Property | Type | Default |
|---|---|---|
HorizontalAlign | Fill, Left, Center, Right | Fill |
VerticalAlign | Fill, Top, Center, Bottom | Fill |
WordWrap | Yes, No | Yes |
HorizontalTextAnchor | None, Left, Center, Right | None |
VerticalTextAnchor | Top, Center, Bottom | Top |
TextFormat | TextFormat{ ... } - what the control's text is laid out over; several compose kind by kind. See the Text in Controls manual | none |
Padding, Spacing | { x, y } or one float | the 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 together | unset |
Border | { Thickness } | None, or the theme block's where one was passed |
Radius | { value } | zero, or the theme block's where one was passed |
ControlMetrics | the whole block, from themeMetrics() | zero on every field but maxSize |
Container properties
| Container | Property | Values |
|---|---|---|
Stack | Orientation | Horizontal, Vertical, HorizontalWrap, VerticalWrap |
LaneSize | { n } - items per lane | |
ItemSizing | Natural, Equal | |
ScrollBox | ScrollBars | Both, Vertical, Horizontal, None, Auto |
PageControl | PageSizing | CurrentPage, WidestPage |
TabbedBox | TabsOrientation | HorizontalTop, HorizontalBottom, VerticalLeft, VerticalRight |
TabLineThickness | { Thickness } - Thin unless stated | |
TabViewMode | Tab, ToolButton | |
Expander | ExpanderViewMode | Section, Divider, Ellipsis, TreeNode |
PanelBase | TextPlacement | Body, Top, Left |
| a grid column | ColumnWidthMode | FitContent, Fixed, Fill |
Building the tree
| Call | Answers |
|---|---|
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 |
add- or create- call of the control that will own it, which is what
gives it its parent and its form.
Reading a layout
| Member | Answers |
|---|---|
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 |