Writing a custom layout
When no standard layout places things the way you want, write your own. A
layout is any type that implements
core.Layout,
which has two methods:
| Method | Job |
|---|---|
MinSize(objects []core.CanvasObject) geo.Size | Report the smallest size that fits all the children. |
Layout(objects []core.CanvasObject, size geo.Size) | Call Resize and Move on each child to place it inside size. |
The container calls both. objects is the container’s child list, and
positions are relative to the container’s top-left corner.
An example: keep an aspect ratio
This layout makes its children as large as possible at a fixed width to height ratio, and centres them. It is the shape of a video player or a photo frame.
// aspectLayout gives every child the largest size with the given
// width/height ratio that fits, centred in the container.
type aspectLayout struct {
ratio float32
}
// Layout positions and sizes the children. It runs whenever the
// container is resized or refreshed.
func (l aspectLayout) Layout(objects []core.CanvasObject, size geo.Size) {
w, h := size.Width, size.Width/l.ratio
if h > size.Height {
w, h = size.Height*l.ratio, size.Height
}
pos := geo.NewPos((size.Width-w)/2, (size.Height-h)/2)
for _, o := range objects {
if !o.Visible() {
continue
}
o.Resize(geo.NewSize(w, h))
o.Move(pos)
}
}
// MinSize is the smallest size the container can have: big enough for
// every visible child's own minimum, at the right ratio.
func (l aspectLayout) MinSize(objects []core.CanvasObject) geo.Size {
var min geo.Size
for _, o := range objects {
if o.Visible() {
min = min.Max(o.MinSize())
}
}
if min.Width/l.ratio < min.Height {
min.Width = min.Height * l.ratio
} else {
min.Height = min.Width / l.ratio
}
return min
}Use it with container.New, like any other layout:
screen := canvas.NewRectangle(color.NRGBA{R: 0x20, G: 0x20, B: 0x28, A: 0xff})
caption := widget.NewLabel("16:9")
video := container.New(aspectLayout{ratio: 16.0 / 9.0}, screen, container.NewCenter(caption))Given a 600×500 container, the rectangle becomes 600×337.5, centred vertically. In a wide, short space it is limited by the height instead.
When the methods run
Layoutruns when the container is resized, when its children change throughAdd,RemoveorRemoveAll, and when the container is refreshed.MinSizeruns whenever something asks the container for its minimum size, which happens often: every time a parent lays itself out. Keep it cheap.
Guidelines
- Skip hidden children. The standard layouts ignore objects whose
Visible()is false, both when placing and when measuring. Users expectHide()to take a child out of the layout. - Use the children’s minimum sizes. Do not make a child smaller than its
MinSize()unless that is the point of your layout; the content will not fit. - Make
MinSizeagree withLayout. IfMinSizereports less thanLayoutneeds, children are squeezed or overlap when the container is at its minimum size. - Space with the theme. For gaps between children, use
theme.Padding(), as the standard layouts do, so your layout matches the rest of the app. - Store settings in the layout value. Configuration such as the ratio above lives in the layout’s fields. One layout value can serve several containers if it has no per-container state.
A layout that needs to react to input or draw something of its own is no
longer just a layout. Write a custom widget
instead; its renderer has a Layout method of its own.
The whole program
// A custom layout that keeps its children at a fixed aspect ratio, as large
// as fits, centred in the space.
package main
import (
"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/widget"
)
// aspectLayout gives every child the largest size with the given
// width/height ratio that fits, centred in the container.
type aspectLayout struct {
ratio float32
}
// Layout positions and sizes the children. It runs whenever the
// container is resized or refreshed.
func (l aspectLayout) Layout(objects []core.CanvasObject, size geo.Size) {
w, h := size.Width, size.Width/l.ratio
if h > size.Height {
w, h = size.Height*l.ratio, size.Height
}
pos := geo.NewPos((size.Width-w)/2, (size.Height-h)/2)
for _, o := range objects {
if !o.Visible() {
continue
}
o.Resize(geo.NewSize(w, h))
o.Move(pos)
}
}
// MinSize is the smallest size the container can have: big enough for
// every visible child's own minimum, at the right ratio.
func (l aspectLayout) MinSize(objects []core.CanvasObject) geo.Size {
var min geo.Size
for _, o := range objects {
if o.Visible() {
min = min.Max(o.MinSize())
}
}
if min.Width/l.ratio < min.Height {
min.Width = min.Height * l.ratio
} else {
min.Height = min.Width / l.ratio
}
return min
}
func main() {
a := app.NewWithID("com.example.customlayout")
w := a.NewWindow("Custom layout")
screen := canvas.NewRectangle(color.NRGBA{R: 0x20, G: 0x20, B: 0x28, A: 0xff})
caption := widget.NewLabel("16:9")
video := container.New(aspectLayout{ratio: 16.0 / 9.0}, screen, container.NewCenter(caption))
w.SetContent(video)
w.Resize(geo.NewSize(600, 500))
bellui.ShowAndRun(w)
}