Skip to content

Guide

Layout engine #

Knots calculates the size and position of each element after your frame function returns. The model is similar to CSS flexbox, with grids, layers, and absolute positions.

The model #

Each component makes one or more elements. An element is a box with a size, padding, and children. You set layout with these ui.Style fields:

Field Values Default
width, height .fixed(px), .fit(), .grow(), .percent(f) .fit()
direction .row, .column, .layer, .grid .row
padding .all(v), .xy(x, y), .init(top, right, bottom, left) 0
gap Pixels between children 0
justify .start, .center, .end, .space_between, .space_around .start
@"align" .start, .center, .end, .stretch .start
overflow .visible, .hidden, .scroll, .scroll_x, .scroll_y .visible
position, offset .static, .absolute; .{ x, y } .static
layer .base, .dropdown, .popup, .modal .base
grid, grid_cell Track template; cell placement —

All values are in logical pixels. Knots multiplies them by the content scale of the display when it draws.

Sizing #

Size Result
.fixed(px) Exactly px.
.fit() The size of the content: children, gaps, and padding. For text and images, the size of the text or image.
.grow() A share of the free space in the parent.
.percent(f) A fraction of the parent content size. f is from 0 to 1. For example, .percent(0.5) is half.

Minimum and maximum #

Each size also has min and max fields. The helper functions do not set them, so use a struct literal:

.width = .{ .kind = .grow, .min = 200, .max = 600 },
.height = .{ .kind = .fit, .max = 400 },

Limit: a .grow() size on the cross axis fills the parent and does not obey max. For example, in a column, a child width of .{ .kind = .grow, .max = 600 } becomes the full column width. To limit it, put the child in a row, or calculate a .fixed width.

How layout runs #

Layout has three passes:

  1. Measure, from the leaves to the root. Knots calculates each .fixed and .fit size. A .fit row adds the widths of its children and the gaps. A .fit column uses the widest child. Then Knots applies min and max.
  2. Place, from the root to the leaves. For each parent, Knots resolves .percent sizes. Then it divides the free space on the main axis equally between the .grow children. If a child reaches its min or max, Knots gives the remaining space to the other children. Then it positions the children with justify and @"align".
  3. Wrap text. Text with wrap = true now has a width. Knots breaks the text into lines at that width. If a text height changes, Knots runs the layout again, so that the parents fit the new height.

The main axis is horizontal for .row and vertical for .column. justify works on the main axis. @"align" works on the other axis, the cross axis.

.stretch alignment places children at the start. It does not resize them. To fill the cross axis, give the child a .grow() size on that axis.

Rows and columns #

This toolbar puts a title at the left and two buttons at the right:

try frame.e(.{
    Rect{ .key = .src(@src()), .style = &.{
        .width = .grow(),
        .direction = .row,
        .@"align" = .center,
        .gap = 8,
        .padding = .xy(12, 8),
    } },
    .{
        Text{ .key = .src(@src()), .content = "Documents" },
        Spacer{ .key = .src(@src()), .style = &.{ .width = .grow() } },
        Button{ .key = .src(@src()), .label = "New" },
        Button{ .key = .src(@src()), .label = "Open" },
    },
});

The Spacer takes all free space, so the buttons move to the right. .justify = .space_between is a different way to do this.

Grids #

Set direction = .grid and a grid template. A track is .{ .fixed = px } or .{ .fr = n }. Knots gives the fixed tracks their size first. Then it divides the remaining space between the fr tracks in proportion to n.

const columns = [_]ui.layout.Grid.Track{ .{ .fixed = 160 }, .{ .fr = 1 }, .{ .fr = 2 } };
const rows = [_]ui.layout.Grid.Track{ .{ .fixed = 48 }, .{ .fr = 1 } };

try frame.e(.{
    Rect{ .key = .src(@src()), .style = &.{
        .width = .grow(),
        .height = .grow(),
        .direction = .grid,
        .gap = 8,
        .grid = .{ .cols = &columns, .rows = &rows },
    } },
    .{
        Rect{ .key = .src(@src()), .style = &.{ .grid_cell = .{ .row = 0, .col = 0, .col_span = 3 } } },
        Rect{ .key = .src(@src()), .style = &.{ .grid_cell = .{ .row = 1, .col = 0 } } },
        Rect{ .key = .src(@src()), .style = &.{ .grid_cell = .{ .row = 1, .col = 1, .col_span = 2 } } },
    },
});

A child without grid_cell goes in row 0, column 0.

Layers and absolute positions #

direction = .layer puts all children on top of each other in the same area. justify sets the horizontal position and @"align" sets the vertical position. Use it for badges on icons or text on images.

position = .absolute removes a child from the flow of its parent. Knots puts the child at the content origin of the parent plus offset. The child does not change the size of the parent.

The layer field is different: it sets the drawing order. Elements on a higher layer draw above elements on a lower layer and get pointer input first. The named layers are .base (0), .dropdown (1), .popup (10), and .modal (200). Use ui.Layer.fromIndex(n) for other values. Layers 240 and higher are for the host.

Overflow and scrolling #

  • .visible lets children draw outside the element.
  • .hidden clips the children to the element.
  • .scroll, .scroll_x, and .scroll_y clip the children and let the user scroll. Knots shows a scrollbar when the content is larger than the element. The scroll offset is widget state, so it stays between frames.

A scroll area must have a size that does not come from its content. Use .fixed or .grow() on the scroll axis. A .fit() element is always as large as its content, so it never scrolls.

The theme sets the scrollbar thickness and colors. Read Themes.