Layout
Goal: At the end of this chapter you can build a screen that grows and shrinks with its window – a top bar, a list, an editor, a status line – without placing a single element by hand.
Containers do the work
In most UI toolkits of the past you placed every element yourself: this button at x 420, y 12, 80 pixels wide. Then the user made the window bigger, and everything stayed in the top-left corner.
Forge works the other way round. You put elements into containers, and the containers decide where their children go. You only say what you want: these side by side, that one as wide as possible, this one at the bottom. That is the Intention Language again – applied to space.
Four containers cover most screens:
| Container | What it does with its children |
|---|---|
VBoxContainer |
stacks them vertically, top to bottom |
HBoxContainer |
puts them side by side, left to right |
GridContainer |
fills a grid, row by row, with a fixed number of columns |
HSplitContainer |
two children side by side, with a handle between them the user can drag |
Containers can be nested as deep as you like. A real screen is usually a VBoxContainer of HBoxContainers, with one more container inside where it is needed.
The example
A notes app – only the layout for now, the behaviour comes in Part III.
app.sml:
Window {
id: mainWindow
title: "Notes"
size: 640, 420
VBoxContainer {
anchors: left | top | right | bottom
padding: 12, 12, 12, 12
spacing: 8
// Top bar: the title takes all free space, the buttons keep their size
HBoxContainer {
spacing: 8
Label { text: "My Notes" fontSize: 22 fontWeight: bold sizeFlagsHorizontal: expandFill }
Button { id: btnNew text: "New" }
Button { id: btnDelete text: "Delete" }
}
// Middle: list on the left, editor on the right; takes all free height
HSplitContainer {
sizeFlagsVertical: expandFill
VBoxContainer {
width: 180
spacing: 4
Button { text: "Shopping list" }
Button { text: "Ideas for the weekend" }
Button { text: "Book chapter" }
}
TextEdit {
id: editor
text: "Milk\nBread\nCoffee"
sizeFlagsHorizontal: expandFill
}
}
// Bottom bar
HBoxContainer {
Label { id: status text: "3 notes" sizeFlagsHorizontal: expandFill }
Label { text: "saved" color: "#7FD1AE" }
}
}
}
app.sms stays empty for now – create the file anyway, so you can add behaviour later.
Start it and make the window bigger:

The title stays on the left, the buttons stay on the right. The editor takes every new pixel, in both directions. The status line stays at the bottom. Not one element has a position.
The outer container
VBoxContainer {
anchors: left | top | right | bottom
padding: 12, 12, 12, 12
spacing: 8
The window has exactly one child: a VBoxContainer that holds the three rows – top bar, middle, bottom bar.
anchors: left | top | right | bottom glues the edges of the container to the edges of the window. When the window grows, the container grows with it. Without this line the container would only be as big as its content and sit in the top-left corner. You will write this line in almost every app; it is the first line of the outer container.
padding is the space between the edge of the container and its children – 12 pixels on every side here. (Remember the order: left, top, right, bottom.)
spacing is the space between the children: 8 pixels between the top bar and the middle, 8 between the middle and the bottom bar.
Size flags: who gets the free space?
A container first gives every child the space it needs: a button needs room for its text, a label for its letters. Then there is usually space left over. Size flags decide who gets it.
Label { text: "My Notes" ... sizeFlagsHorizontal: expandFill }
Button { id: btnNew text: "New" }
Button { id: btnDelete text: "Delete" }
In the top bar, the label says expandFill: give me all the horizontal space nobody else needs. The buttons say nothing, so they keep their natural width. The result: the title fills the bar and pushes both buttons to the right edge.
The same trick works vertically:
HSplitContainer {
sizeFlagsVertical: expandFill
The outer VBoxContainer has three rows. Top and bottom bar take their natural height; the middle row says expandFill and gets everything in between. That is why the bottom bar always sits at the bottom of the window.
There are two flags, one per direction:
sizeFlagsHorizontal– how the element uses widthsizeFlagsVertical– how the element uses height
And these values:
| Value | Meaning |
|---|---|
fill |
use the full space of your own slot (the default) |
expandFill |
ask for the free space, and fill it |
shrinkCenter |
only as big as needed, centred in the slot |
shrinkEnd |
only as big as needed, at the end of the slot (right or bottom) |
If several children say expandFill, they share the free space equally. Try it: give the second button sizeFlagsHorizontal: expandFill too, and watch the title and the button split the bar.
Size flags are a request to the parent. A sizeFlagsVertical on an element inside an HBoxContainer decides how it fills the height of the row – the HBoxContainer only distributes width among its children, the height comes from its own parent.
Minimum sizes
VBoxContainer {
width: 180
...
}
width and height set the minimum size of an element. The list on the left is at least 180 pixels wide; if its content needs more, it gets more. In an HSplitContainer, the user can drag the handle to make it wider – but never narrower than 180.
That word minimum is important. width: 180 does not fix the width. Combined with expandFill, an element starts at its minimum and grows from there.
The split container
HSplitContainer {
sizeFlagsVertical: expandFill
VBoxContainer { width: 180 ... }
TextEdit { ... sizeFlagsHorizontal: expandFill }
}
An HSplitContainer takes exactly two children and puts a handle between them. The editor says expandFill, so it takes all the width the list does not need. Drag the handle and the space moves from one side to the other. (VSplitContainer does the same top over bottom.)
Grids
For anything that looks like a table of equal cells – a keypad, a set of tiles, a form with labels and fields – use a GridContainer:
GridContainer {
columns: 3
spacing: 6
Button { text: "7" } Button { text: "8" } Button { text: "9" }
Button { text: "4" } Button { text: "5" } Button { text: "6" }
}
The grid fills its cells row by row: after columns children it starts a new row. You do not write rows yourself. (Putting three buttons on one line in the file is only to make the shape visible to you – the grid does not care about line breaks.)
Tabs
A TabContainer shows one child at a time and a row of tabs on top to switch between them. The title of each tab is set on the child, with the container's name in front:
TabContainer {
VBoxContainer { TabContainer.title: "General" Label { text: "General settings" } }
VBoxContainer { TabContainer.title: "Advanced" Label { text: "Advanced settings" } }
}
TabContainer.title is a property that belongs to the relationship between child and parent: the page itself has no title, but as a page of a tab container it does.
When you really need a position: anchors
Containers are the normal way. Sometimes, though, an element should float at a fixed place in the window – a round add button in the bottom-right corner, a logo in the top-left. For that, you place the element directly inside the window and use anchors and offsets.
Window {
id: mainWindow
title: "Corner"
size: 480, 320
Label {
text: "Anchored to the top left"
anchors: left | top
offsetLeft: 16
offsetTop: 16
}
Button {
id: btnAdd
text: "+"
fontSize: 28
anchorLeft: 1
anchorTop: 1
anchorRight: 1
anchorBottom: 1
offsetLeft: -72
offsetTop: -72
offsetRight: -16
offsetBottom: -16
}
}

Every element has four edges, and each edge is tied to an anchor – a point in the parent, given as a number:
0– the left edge (foranchorLeft/anchorRight) or the top edge (foranchorTop/anchorBottom) of the parent1– the right or bottom edge of the parent0.5– the middle
The offset is the distance in pixels from that anchor. For the button, all four anchors are 1, so all four edges hang on the bottom-right corner of the window. The offsets then cut a 56 × 56 box out of the corner, 16 pixels away from both edges: the left edge 72 pixels left of the corner, the right edge 16 pixels left of it, the same for top and bottom.
When the window grows, the corner moves – and the button moves with it, at the same distance.
The short form anchors: left | top | right | bottom you already know sets the named edges: left and top to 0, right and bottom to 1. With all four, the element stretches over the whole parent – exactly what you want for the outer container. For an element that should not stretch, set the four anchors one by one, as the button does.
Pitfalls
Everything sits in the top-left corner and does not grow. The outer container has no anchors. Add anchors: left | top | right | bottom.
The bottom bar is not at the bottom. No row in between says sizeFlagsVertical: expandFill, so the free height ends up below the last row. Give the row that should grow the flag.
expandFill does nothing. The flag must be in the right direction for the parent: inside an HBoxContainer, only sizeFlagsHorizontal shares out free space; inside a VBoxContainer, only sizeFlagsVertical.
width: 180 and the element is wider. width is a minimum, not a fixed size. Content that needs more space gets it.
An anchored button fills half the window. You wrote anchors: right | bottom for a single element. That ties the right and bottom edges to the corner, but leaves the left and top edges where they were – at the top-left. Set all four anchors explicitly, as in the corner example.
alignment: center on a box has no effect. In the current Runner, alignment on HBoxContainer and VBoxContainer is not applied yet. To centre a group of buttons, put an empty, expanding element on both sides: Control { sizeFlagsHorizontal: expandFill }.
Exercise
Build the layout of a pocket calculator in a window of 300 × 400:
- A display at the top: one
Labelwith the text"0", font size 32, right-aligned (align: right). - Below it, a keypad with 12 buttons in three columns:
7 8 9,4 5 6,1 2 3,C 0 =. - The keypad fills all the rest of the window, and every button grows with it – make the window bigger, and the buttons get bigger.
Hint: a button grows in a grid cell only when it asks for it, in both directions.
The solution is in the appendix.
What you learned
- Containers place their children; you describe intentions, not coordinates.
VBoxContainer,HBoxContainer,GridContainerandHSplitContainercover most screens; nest them freely.anchors: left | top | right | bottommakes the outer container follow the window.paddingis space inside the edge (left, top, right, bottom),spacingis space between children.sizeFlagsHorizontal/sizeFlagsVerticaldecide who gets the free space:expandFilltakes it,shrinkCenterandshrinkEndkeep the natural size.widthandheightare minimum sizes.TabContainer.titlenames a tab.- For a floating element, set the four anchors (
0,0.5,1) and the four offsets in pixels.
Next: Resources and Themes
