Layout

July 9, 2026 · View on GitHub

Widgets stack vertically by default. Everything else is opt-in.


Gap — Put Space Between Things

The most-used layout function. Inserts blank vertical space.

proton.H4(ctx, "Section Title")
proton.Gap(ctx, 8)
proton.Label(ctx, "Section content.")
proton.Gap(ctx, 24)
proton.H4(ctx, "Next Section")
proton.Gap(ctx proton.Context, dp float32)

8dp is a small gap. 16dp is medium. 24dp is large. Those three cover most cases.


Row — Side by Side

Places children horizontally, left to right.

proton.Row(ctx,
    func(ctx proton.Context) { proton.Label(ctx, "Name:") },
    func(ctx proton.Context) { proton.Gap(ctx, 8) },
    func(ctx proton.Context) { proton.Label(ctx, "Alice") },
)

Each child is a func(proton.Context). Call whatever widgets you want inside it.

proton.Row(ctx proton.Context, widgets ...func(proton.Context))

Column — Explicit Vertical Group

Stacks children vertically as a named group. Rarely needed at the top level (widgets stack automatically), but useful inside Row or Split when the right side needs to be multiple stacked things.

proton.Row(ctx,
    func(ctx proton.Context) {
        proton.Label(ctx, "Left side")
    },
    func(ctx proton.Context) { proton.Gap(ctx, 16) },
    func(ctx proton.Context) {
        proton.Column(ctx,
            func(ctx proton.Context) { proton.Label(ctx, "Right top") },
            func(ctx proton.Context) { proton.Gap(ctx, 4) },
            func(ctx proton.Context) { proton.Muted(ctx, "Right bottom") },
        )
    },
)
proton.Column(ctx proton.Context, widgets ...func(proton.Context))

RowSpread — Space Between

Like Row but puts leftover horizontal space between the children, pushing the first to the left edge and the last to the right.

// title on the left, version on the right
proton.RowSpread(ctx,
    func(ctx proton.Context) { proton.H5(ctx, "My App") },
    func(ctx proton.Context) { proton.Caption(ctx, "v1.2.0") },
)
proton.RowSpread(ctx proton.Context, widgets ...func(proton.Context))

RowEnd — Everything on the Right

Pushes all children to the right edge.

proton.RowEnd(ctx,
    func(ctx proton.Context) {
        proton.Pad(ctx, 4, func(ctx proton.Context) {
            if proton.OutlineButton(ctx, &u.cancel, "Cancel") { handleCancel() }
        })
    },
    func(ctx proton.Context) { proton.Gap(ctx, 8) },
    func(ctx proton.Context) {
        proton.Pad(ctx, 4, func(ctx proton.Context) {
            if proton.Button(ctx, &u.save, "Save") { handleSave() }
        })
    },
)
proton.RowEnd(ctx proton.Context, widgets ...func(proton.Context))

GrowRow and GrowColumn — Stretchy Layouts

When one child needs to fill all remaining space and the others stay their natural size, use GrowRow (horizontal) or GrowColumn (vertical) with GrowItem and FixedItem.

// search bar: label fixed, input stretches, button fixed
proton.GrowRow(ctx,
    proton.FixedItem(ctx, func(ctx proton.Context) {
        proton.Label(ctx, "Search:")
    }),
    proton.GrowItem(ctx, func(ctx proton.Context) {
        proton.Input(ctx, &u.search, "Type to search...")
    }),
    proton.FixedItem(ctx, func(ctx proton.Context) {
        proton.Pad(ctx, 4, func(ctx proton.Context) {
            if proton.Button(ctx, &u.searchBtn, "Go") { doSearch() }
        })
    }),
)

GrowItem takes all remaining space. FixedItem takes only what it needs. Multiple GrowItems split remaining space evenly.

proton.GrowRow(ctx proton.Context, children ...proton.FlexItem)
proton.GrowColumn(ctx proton.Context, children ...proton.FlexItem)
proton.GrowItem(ctx proton.Context, fn func(proton.Context)) proton.FlexItem
proton.FixedItem(ctx proton.Context, fn func(proton.Context)) proton.FlexItem

FlexSpacer — Push siblings apart

A stretchy empty space. Put it between FixedItems to push them to opposite edges without using RowSpread.

proton.GrowRow(ctx,
    proton.FixedItem(ctx, func(ctx proton.Context) { proton.Caption(ctx, "left") }),
    proton.FlexSpacer(),
    proton.FixedItem(ctx, func(ctx proton.Context) { proton.Caption(ctx, "right") }),
)
proton.FlexSpacer() proton.FlexItem

Split — Side-by-Side Panes

Divides available width between two sections. leftFraction is the proportion the left pane gets, from 0.0 to 1.0.

proton.Split(ctx, 0.35,
    func(ctx proton.Context) {
        // sidebar — gets 35% of the width
        proton.Label(ctx, "Sidebar")
    },
    func(ctx proton.Context) {
        // content — gets the remaining 65%
        proton.Label(ctx, "Content")
    },
)
proton.Split(ctx proton.Context, leftFraction float32, left func(proton.Context), right func(proton.Context))

HSplit — Top and Bottom

Same idea but vertical.

proton.HSplit(ctx, 0.7,
    func(ctx proton.Context) { proton.Label(ctx, "Main content") },
    func(ctx proton.Context) { proton.Label(ctx, "Status bar") },
)
proton.HSplit(ctx proton.Context, topFraction float32, top func(proton.Context), bottom func(proton.Context))

ResizeSplit — User Can Drag the Divider

Like Split but the user can drag the handle between the two panes to resize them. The defaultFraction is the initial position.

type UI struct {
    split proton.ResizeSplitState
}

proton.ResizeSplit(ctx, &u.split, 0.30, leftFn, rightFn)

ResizeSplitState.Fraction starts at 0 and gets set to defaultFraction on the first frame. After that the user's drag position is remembered.

proton.ResizeSplit(ctx proton.Context, state *proton.ResizeSplitState, defaultFraction float32, left func(proton.Context), right func(proton.Context))
proton.ResizeHSplit(ctx proton.Context, state *proton.ResizeSplitState, defaultFraction float32, top func(proton.Context), bottom func(proton.Context))

Center

Places content in the center of available space. Great for empty states and loading screens.

proton.Center(ctx, func(ctx proton.Context) {
    proton.Muted(ctx, "Nothing here yet.")
})
proton.Center(ctx proton.Context, fn func(proton.Context))

Padding

Pad — All Four Sides

proton.Pad(ctx, 16, func(ctx proton.Context) {
    proton.Label(ctx, "16dp of breathing room on all sides")
})

PadH — Left and Right Only

proton.PadH(ctx, 24, func(ctx proton.Context) {
    proton.Label(ctx, "horizontal padding only")
})

PadV — Top and Bottom Only

proton.PadV(ctx, 12, func(ctx proton.Context) {
    proton.Label(ctx, "vertical padding only")
})

PadSides — Each Edge Individually

Arguments are top, right, bottom, left — same order as CSS margin/padding.

proton.PadSides(ctx, 8, 16, 8, 16, func(ctx proton.Context) {
    proton.Label(ctx, "8dp top/bottom, 16dp left/right")
})
proton.Pad(ctx proton.Context, dp float32, fn func(proton.Context))
proton.PadH(ctx proton.Context, dp float32, fn func(proton.Context))
proton.PadV(ctx proton.Context, dp float32, fn func(proton.Context))
proton.PadSides(ctx proton.Context, top, right, bottom, left float32, fn func(proton.Context))

Grid — Fixed-Column Grid

Arranges children in a grid with a fixed number of columns. Each cell gets an equal share of the available width.

proton.Grid(ctx, 3, 8,   // 3 columns, 8dp gap
    func(ctx proton.Context) { proton.Label(ctx, "one") },
    func(ctx proton.Context) { proton.Label(ctx, "two") },
    func(ctx proton.Context) { proton.Label(ctx, "three") },
    func(ctx proton.Context) { proton.Label(ctx, "four") },
    func(ctx proton.Context) { proton.Label(ctx, "five") },
)

Cells wrap onto new rows automatically. If the last row has fewer than cols cells, the remaining slots are empty.

proton.Grid(ctx proton.Context, cols int, gapDp float32, cells ...func(proton.Context))

ZStack — Draw Things on Top of Each Other

Layers multiple widgets at the same position. The first child is on the bottom; the last is on top.

proton.ZStack(ctx,
    func(ctx proton.Context) {
        // bottom layer — a background shape
        proton.RoundRect(ctx, proton.RGB(0x1e1e2e), 0, 100, 12)
    },
    func(ctx proton.Context) {
        // top layer — text floating over the shape
        proton.Center(ctx, func(ctx proton.Context) {
            proton.Label(ctx, "Text on top")
        })
    },
)
proton.ZStack(ctx proton.Context, layers ...func(proton.Context))

MinSize and MaxWidth — Size Constraints

// at least 200dp wide and 48dp tall
proton.MinSize(ctx, 200, 48, func(ctx proton.Context) {
    if proton.Button(ctx, &u.btn, "OK") { handleOK() }
})

// no wider than 420dp — keeps forms readable on wide windows
proton.MaxWidth(ctx, 420, func(ctx proton.Context) {
    proton.Input(ctx, &u.email, "Email address")
    proton.Gap(ctx, 8)
    proton.Input(ctx, &u.password, "Password")
})
proton.MinSize(ctx proton.Context, widthDp, heightDp float32, fn func(proton.Context))
proton.MaxWidth(ctx proton.Context, widthDp float32, fn func(proton.Context))

Pass 0 for either dimension of MinSize to leave that axis unconstrained.


A Typical Two-Column App

func draw(ctx proton.Context, u *UI) {
    // header
    proton.PadSides(ctx, 0, 0, 12, 0, func(ctx proton.Context) {
        proton.RowSpread(ctx,
            func(ctx proton.Context) { proton.H5(ctx, "My App") },
            func(ctx proton.Context) { proton.Caption(ctx, "v1.0") },
        )
    })
    proton.Divider(ctx)
    proton.Gap(ctx, 16)

    // body
    proton.ResizeSplit(ctx, &u.split, 0.28,
        func(ctx proton.Context) {
            drawSidebar(ctx, u)
        },
        func(ctx proton.Context) {
            proton.PadH(ctx, 16, func(ctx proton.Context) {
                drawContent(ctx, u)
            })
        },
    )
}