Skip to content
Containers and layouts

Containers and layouts

Each section below shows one container, taken from a single program that draws them all. In the examples, box(name) is a small helper that returns a tinted rectangle with a label on it.

ConstructorChildren areUse it for
NewVBox, NewHBoxIn a column or a row, at their minimum height (or width)Lists of controls, toolbars
NewBorderAround the edges, with the rest in the middleThe outer frame of most screens
NewGridWithColumns, NewGridWithRowsIn equal cellsButton pads, dashboards
NewGridWrapAt one fixed size, wrapping onto new rowsThumbnails, tiles
NewCenterAt their minimum size, centredA single control in a large space
NewStackOn top of each other, all full sizeBackgrounds, overlays
NewPaddedFull size, inset by the theme paddingBreathing room around content
New(layout.NewFormLayout(), ...)In label and value columnsForms

VBox and HBox

container.NewVBox places children top to bottom. Each child is as wide as the container and as tall as its own minimum height.

// Top to bottom. Each child is full width and only as tall as it needs.
return container.NewVBox(box("One"), box("Two"), box("Three"))

container.NewHBox does the same left to right: each child is as tall as the container and as wide as its minimum width.

A layout.NewSpacer() in a box takes up the free space. Two spacers share it equally. Use one to push items to the far end, or two to centre something:

// Left to right. The spacer takes the free space, pushing "Right" to the end.
return container.NewHBox(box("Left"), layout.NewSpacer(), box("Right"))

Boxes never stretch a child along their main direction. If you want a child to fill the remaining space, use a Border instead. Between children, boxes put the theme’s padding; layout.NewCustomPaddedVBoxLayout(p) and NewCustomPaddedHBoxLayout(p) use your own value.

Border

container.NewBorder(top, bottom, left, right, objects...) puts the first four objects along the edges and stretches the rest over the middle. Pass nil for an edge you do not need.

// Edges first (top, bottom, left, right; nil leaves one empty),
// then the content that fills the middle.
return container.NewBorder(box("Top"), box("Bottom"), box("L"), nil, box("Middle"))

The top and bottom objects get their minimum height and the full width. The left and right objects get their minimum width and the height between top and bottom. Everything after the fourth argument fills the middle; if you pass several, they are stacked on top of each other.

A Border is the usual frame for a screen: a toolbar on top, a status line at the bottom, and a scrolling list in the middle that gets all the remaining space.

Grid

container.NewGridWithColumns(n, ...) lays children out in n columns, adding rows as needed. Every cell is the same size, and each child fills its cell.

// Equal-sized cells, two per row. Rows are added as needed.
return container.NewGridWithColumns(2, box("1"), box("2"), box("3"), box("4"))

container.NewGridWithRows(n, ...) fixes the number of rows instead. container.NewAdaptiveGrid(n, ...) switches between the two with the device orientation; see Responsive layouts.

GridWrap

container.NewGridWrap(size, ...) gives every child the same fixed size and places them in rows, wrapping to a new row when the next one does not fit.

// Every child gets the same fixed size; they flow onto new rows
// when the width runs out.
var tiles []core.CanvasObject
for i := 1; i <= 6; i++ {
	tiles = append(tiles, box(fmt.Sprint(i)))
}
return container.NewGridWrap(geo.NewSize(50, 40), tiles...)

When the children have different natural widths, such as a set of tags, use layout.NewRowWrapLayout() instead; see Responsive layouts.

Center

container.NewCenter keeps each child at its minimum size, in the middle of the space.

// The child keeps its minimum size and sits in the middle.
return container.NewCenter(widget.NewButton("Centred", func() {}))

Stack

container.NewStack makes every child the full size of the container and draws them in order, so later children are on top. Use it to put a background behind something, or a badge over it.

// Children on top of each other, all at full size. Later ones are on top.
bg := canvas.NewVerticalGradient(
	color.NRGBA{R: 0x3b, G: 0x6e, B: 0xc8, A: 0xff},
	color.NRGBA{R: 0xe8, G: 0x8a, B: 0x2c, A: 0xff})
return container.NewStack(bg, container.NewCenter(widget.NewLabel("On top")))

Padded

container.NewPadded makes each child the full size of the container minus the theme’s padding on all four sides.

// The child fills the space minus the theme's padding on every side.
return container.NewPadded(box("Padded"))

For different padding on each side, use container.New(layout.NewCustomPaddedLayout(top, bottom, left, right), ...).

Form

The form layout has two columns. Children are taken in pairs: the first of each pair is the label, the second its value. The label column is as wide as the widest label.

// Two columns: labels on the left, content on the right.
return container.New(layout.NewFormLayout(),
	widget.NewLabel("Name"), widget.NewEntry(),
	widget.NewLabel("Email"), widget.NewEntry(),
)

For a form with a submit button and validation, use the widget.Form widget instead.

Other layouts

The layout package has a few more, used with container.New:

  • NewFlexHBoxLayout(flex) and NewFlexVBoxLayout(flex): a row or column where each child has a grow factor. Children with factor 0 keep their minimum size; the rest share the free space in proportion. The MinWidth and MaxWidth fields (or MinHeight and MaxHeight) clamp individual children.
  • NewShrinkLayout(): reports a minimum size of zero and fills the space. Wrap content in it when a parent such as a split must be able to shrink below the content’s natural minimum size.
  • NewResponsiveLayout and NewRowWrapLayout: see Responsive layouts.

Changing a container’s children

A container’s children are in its Objects field. Change them with methods, so the layout runs again:

  • Add(obj) appends a child. It is safe to call from any goroutine.
  • Remove(obj) removes one child, and RemoveAll() removes them all. Call these on the UI thread.

If you assign to Objects directly, call Refresh() afterwards. Hide() on a child removes it from the layout: the layouts above skip hidden children.

The whole program

// Every standard container in one window, each in its own titled cell.
package main

import (
	"fmt"
	"image/color"

	"github.com/belluikit/bellui"
	"github.com/belluikit/bellui/app"
	"github.com/belluikit/bellui/canvas"
	"github.com/belluikit/bellui/container"
	"github.com/belluikit/bellui/core"
	"github.com/belluikit/bellui/geo"
	"github.com/belluikit/bellui/layout"
	"github.com/belluikit/bellui/widget"
)

// box is a coloured rectangle with a label, used as sample content.
func box(name string) core.CanvasObject {
	bg := canvas.NewRectangle(color.NRGBA{R: 0x3b, G: 0x6e, B: 0xc8, A: 0x60})
	return container.NewStack(bg, widget.NewLabel(name))
}

func vbox() core.CanvasObject {
	// Top to bottom. Each child is full width and only as tall as it needs.
	return container.NewVBox(box("One"), box("Two"), box("Three"))
}

func hbox() core.CanvasObject {
	// Left to right. The spacer takes the free space, pushing "Right" to the end.
	return container.NewHBox(box("Left"), layout.NewSpacer(), box("Right"))
}

func border() core.CanvasObject {
	// Edges first (top, bottom, left, right; nil leaves one empty),
	// then the content that fills the middle.
	return container.NewBorder(box("Top"), box("Bottom"), box("L"), nil, box("Middle"))
}

func grid() core.CanvasObject {
	// Equal-sized cells, two per row. Rows are added as needed.
	return container.NewGridWithColumns(2, box("1"), box("2"), box("3"), box("4"))
}

func gridWrap() core.CanvasObject {
	// Every child gets the same fixed size; they flow onto new rows
	// when the width runs out.
	var tiles []core.CanvasObject
	for i := 1; i <= 6; i++ {
		tiles = append(tiles, box(fmt.Sprint(i)))
	}
	return container.NewGridWrap(geo.NewSize(50, 40), tiles...)
}

func center() core.CanvasObject {
	// The child keeps its minimum size and sits in the middle.
	return container.NewCenter(widget.NewButton("Centred", func() {}))
}

func stack() core.CanvasObject {
	// Children on top of each other, all at full size. Later ones are on top.
	bg := canvas.NewVerticalGradient(
		color.NRGBA{R: 0x3b, G: 0x6e, B: 0xc8, A: 0xff},
		color.NRGBA{R: 0xe8, G: 0x8a, B: 0x2c, A: 0xff})
	return container.NewStack(bg, container.NewCenter(widget.NewLabel("On top")))
}

func padded() core.CanvasObject {
	// The child fills the space minus the theme's padding on every side.
	return container.NewPadded(box("Padded"))
}

func form() core.CanvasObject {
	// Two columns: labels on the left, content on the right.
	return container.New(layout.NewFormLayout(),
		widget.NewLabel("Name"), widget.NewEntry(),
		widget.NewLabel("Email"), widget.NewEntry(),
	)
}

func main() {
	a := app.NewWithID("com.example.containers")
	w := a.NewWindow("Containers")

	cell := func(title string, content core.CanvasObject) core.CanvasObject {
		heading := widget.NewLabelWithStyle(title, bellui.TextAlignLeading, bellui.TextStyle{Bold: true})
		frame := canvas.NewRectangle(color.Transparent)
		frame.StrokeColor = color.NRGBA{R: 0x80, G: 0x80, B: 0x80, A: 0x80}
		frame.StrokeWidth = 1
		return container.NewBorder(heading, nil, nil, nil,
			container.NewStack(frame, container.NewPadded(content)))
	}

	w.SetContent(container.NewGridWithColumns(3,
		cell("NewVBox", vbox()),
		cell("NewHBox", hbox()),
		cell("NewBorder", border()),
		cell("NewGridWithColumns", grid()),
		cell("NewGridWrap", gridWrap()),
		cell("NewCenter", center()),
		cell("NewStack", stack()),
		cell("NewPadded", padded()),
		cell("FormLayout", form()),
	))
	w.Resize(geo.NewSize(900, 650))
	bellui.ShowAndRun(w)
}