Skip to content
Theme

Layout

Limn has four layout widgets and one marker. That is the whole vocabulary, and it is enough to build the window at the bottom of this page.

Column stacks children top to bottom
Row places children left to right
Stack draws children on top of each other
Padding insets one child
Expanded marks the child that absorbs leftover space

There is no constraint solver to configure and no layout manager to install. Layout runs top-down: a parent hands each child the space it may occupy, the child answers with the size it wants, and the parent places it.

Both take a gap (the gutter between children, applied between them and never before the first or after the last) and a cross alignment, which decides what happens on the other axis.

static Column column(float gap, Widget... children) {
Column column = new Column();
column.gap(gap).crossAlignment(Flex.CrossAlignment.STRETCH);
for (Widget child : children) {
column.add(child);
}
return column;
}

CrossAlignment.STRETCH is the one you will reach for most in a form: it makes every child as wide as the column. The default, START, leaves each child at its own natural width, which is what you want for a row of buttons and almost never what you want for a stack of text fields.

For the main axis there is MainAlignment (START, CENTER, END, SPACE_BETWEEN), which decides where the children sit when there is more room than they need.

Wrap a child in Expanded and it takes what is left over. Give two children different flex values and they share it in that proportion. An Expanded with nothing in it is a spacer, and it is the tidiest way to push a group to the far edge:

static Row spread(Widget leading, Widget trailing) {
Row row = new Row();
row.gap(12).crossAlignment(Flex.CrossAlignment.CENTER);
row.add(leading);
row.add(Expanded.spacer(1));
row.add(trailing);
return row;
}

This is the mechanism behind every “toolbar with actions on the right” and every “sidebar beside a content pane that grows”.

Stack draws its children in order against one shared box, which is what an overlay is: the content, and then the thing on top of it.

static Widget overlay(Widget content, Widget on) {
Stack stack = new Stack();
stack.alignment(Stack.Alignment.CENTER);
stack.add(content);
stack.add(on);
return stack;
}

Every child takes the stack’s alignment, so to nudge one off a corner, wrap it in a Padding rather than looking for a per-child offset.

There is no grid layout, and there does not need to be one: a grid is rows of Expanded children, and writing it yourself takes ten lines and leaves you in charge of what a short last line does.

static Column grid(int perRow, List<Widget> tiles) {
Column rows = new Column();
rows.gap(14).crossAlignment(Flex.CrossAlignment.STRETCH);
for (int start = 0; start < tiles.size(); start += perRow) {
Row line = new Row();
line.gap(14).crossAlignment(Flex.CrossAlignment.START);
for (int i = start; i < Math.min(start + perRow, tiles.size()); i++) {
line.add(Expanded.of(tiles.get(i), 1));
}
for (int i = tiles.size(); i < start + perRow; i++) {
line.add(Expanded.spacer(1));
}
rows.add(line);
}
return rows;
}

Each tile on a line gets flex 1, so the line splits its width evenly however many tiles there are. The second loop is the part worth copying: padding the last line with spacers is what stops two leftover tiles from stretching to half the pane each.

Here is a real window: a toolbar that keeps its own height, a pair of panes that take everything left over, and a status line at the bottom.

public static Widget shell() {
ToolBar bar = new ToolBar();
bar.addItem(new Button("Import"));
bar.addSeparator();
bar.addItem(new Button("New collection").setSecondary(true));
bar.addItem(new Button("Share").setSecondary(true));
SplitPane panes = SplitPane.horizontal(collections(), library());
panes.setRatio(0.24f).setMinimums(150, 320);
Row status = spread(
new Label("18 assets · 3 selected").setMuted(true),
new Label("Synced 2 minutes ago").setMuted(true));
return column(0,
bar,
Expanded.of(panes, 1),
Separator.horizontal(),
new Padding(Insets.symmetric(8, 14), status));
}

Note what is not there. No sizes, no percentages, no anchors. The middle child is the only Expanded one, and that single fact is what routes the window’s spare height into the panes and nowhere else: resize the window and the toolbar and status line stay put while the panes grow.

The window that code produces, rendered by Limn.The window that code produces, rendered by Limn.
The window that code produces, rendered by Limn.

Everything on that screen is on this page. The tiles are the grid above; each one is a column of a stacked swatch-and-badge and two labels; the header row and the status line are both spread. The sidebar is a ListView and the divider is a SplitPane, both of which are covered in Lists and scrolling.

When a widget genuinely has a fixed size (a thumbnail, a fixed-width sidebar, a minimum for a text area), wrap it:

new SizedBox(280, SizedBox.UNSET, textArea)

UNSET on an axis means “leave that one alone”. Use it sparingly: a layout built out of fixed sizes is a layout that breaks at the first long translation.

A centred card. A Row with MainAlignment.CENTER and CrossAlignment.CENTER, one child, and a SizedBox to cap its width.

A two-column form. A Column with STRETCH, where each row is a Row of a fixed-width Label and an Expanded field.

A footer pinned to the bottom. A Column whose middle child is Expanded, the same shape as the window above.