Skip to content
Canvas objects

Canvas objects

Everything you put in a window is a canvas object: a value that implements core.CanvasObject. A button, a container full of widgets and a plain rectangle all implement it, so any of them can go anywhere another one can.

The CanvasObject interface

A canvas object has a size, a position, a minimum size and a visibility:

MethodMeaning
MinSize() geo.SizeThe smallest size the object can be drawn at
Size(), Resize(geo.Size)The current size
Position(), Move(geo.Position)The top-left corner, relative to the parent
Show(), Hide(), Visible()Visibility
Refresh()Redraw the object after you change it

Inside a container you rarely call Resize or Move yourself. The container’s layout does that, using each child’s MinSize.

Sizes and positions are geo.Size (Width, Height) and geo.Position (X, Y), both float32. Create them with geo.NewSize(w, h) and geo.NewPos(x, y). They have helpers such as Add, Subtract, Max and Min. A movement, such as how far a drag went, is a geo.Delta (DX, DY).

The canvas primitives

The canvas package holds the basic drawing objects. They draw something and do nothing else: they do not respond to input. To make something interactive, use a widget or build one from these primitives.

text := canvas.NewText("canvas.Text", theme.Color(theme.ColorNameForeground))
text.TextSize = 20
text.TextStyle = bellui.TextStyle{Bold: true}

rect := canvas.NewRectangle(blue)
rect.CornerRadius = 12
rect.StrokeColor = orange
rect.StrokeWidth = 3

circle := canvas.NewCircle(orange)

line := canvas.NewLine(blue)
line.StrokeWidth = 4

gradient := canvas.NewHorizontalGradient(blue, orange)
radial := canvas.NewRadialGradient(orange, color.Transparent)
TypeCreated withNotes
TextNewText(text, color)A string drawn in one style, with no wrapping or formatting. Set TextSize, TextStyle (bold, italic, monospace) and Alignment. For text that wraps or can be selected, use widget.Label.
RectangleNewRectangle(color)FillColor, StrokeColor, StrokeWidth, and CornerRadius (or one radius per corner).
CircleNewCircle(color)Fills its bounds. FillColor, StrokeColor, StrokeWidth.
LineNewLine(color)Runs from Position1 to Position2. StrokeColor, StrokeWidth.
LinearGradientNewHorizontalGradient, NewVerticalGradient, NewLinearGradient(start, end, angle)Set Stops for more than two colours; it then replaces the start and end colours.
RadialGradientNewRadialGradient(start, end)From the centre outwards.
ImageNewImageFromResource, NewImageFromFile, NewImageFromURI, NewImageFromReader, NewImageFromImageBitmap or SVG. See Resources and images.
RasterNewRaster, NewRasterWithPixels, NewRasterFromImagePixels generated by your function at the size it is drawn.

A raster calls your function to produce its pixels whenever it is drawn at a new size:

// A raster asks for a colour per pixel, at whatever size it is drawn.
checker := canvas.NewRasterWithPixels(func(x, y, w, h int) color.Color {
	if (x/16+y/16)%2 == 0 {
		return color.NRGBA{R: 0xdd, G: 0xdd, B: 0xdd, A: 0xff}
	}
	return color.NRGBA{R: 0x88, G: 0x88, B: 0x88, A: 0xff}
})

An image picks how to fill its space with FillMode: ImageFillStretch (the default, ignores the aspect ratio), ImageFillContain (fits inside, keeps the aspect ratio), ImageFillCover (fills the space and crops) or ImageFillOriginal (asks for the image’s pixel size as its minimum size).

img := canvas.NewImageFromResource(theme.HomeIcon())
img.FillMode = canvas.ImageFillContain

The package has more objects than the table shows, including arcs, polygons, paths and box shadows. See the package reference.

Minimum size of a primitive

Most primitives have a minimum size of 1×1 unless you set one with SetMinSize. Circle and Line have no SetMinSize and always report 1×1. Text is the exception the other way: its minimum size comes from its text and font size, and SetMinSize does nothing. In a layout that sizes children by their minimum, such as a VBox, a rectangle with no minimum size is one unit tall and almost invisible. Either call rect.SetMinSize(geo.NewSize(w, h)), or put it in a layout that stretches its children. The full example uses NewGridWrap to give each primitive a fixed cell:

// The canvas primitives side by side: text, shapes, a line, gradients, a
// raster and an image, next to an ordinary widget.
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/theme"
	"github.com/belluikit/bellui/widget"
)

func main() {
	a := app.NewWithID("com.example.canvas")
	w := a.NewWindow("Canvas objects")

	blue := color.NRGBA{R: 0x3b, G: 0x6e, B: 0xc8, A: 0xff}
	orange := color.NRGBA{R: 0xe8, G: 0x8a, B: 0x2c, A: 0xff}

	text := canvas.NewText("canvas.Text", theme.Color(theme.ColorNameForeground))
	text.TextSize = 20
	text.TextStyle = bellui.TextStyle{Bold: true}

	rect := canvas.NewRectangle(blue)
	rect.CornerRadius = 12
	rect.StrokeColor = orange
	rect.StrokeWidth = 3

	circle := canvas.NewCircle(orange)

	line := canvas.NewLine(blue)
	line.StrokeWidth = 4

	gradient := canvas.NewHorizontalGradient(blue, orange)
	radial := canvas.NewRadialGradient(orange, color.Transparent)

	// A raster asks for a colour per pixel, at whatever size it is drawn.
	checker := canvas.NewRasterWithPixels(func(x, y, w, h int) color.Color {
		if (x/16+y/16)%2 == 0 {
			return color.NRGBA{R: 0xdd, G: 0xdd, B: 0xdd, A: 0xff}
		}
		return color.NRGBA{R: 0x88, G: 0x88, B: 0x88, A: 0xff}
	})

	img := canvas.NewImageFromResource(theme.HomeIcon())
	img.FillMode = canvas.ImageFillContain

	// A widget is a canvas object too, so it goes anywhere the others go.
	button := widget.NewButton("widget.Button", func() {})

	cell := func(o core.CanvasObject) core.CanvasObject {
		// Canvas objects report a minimum size of 1x1 unless told otherwise;
		// a fixed-size cell keeps each one visible.
		return container.NewGridWrap(geo.NewSize(140, 90), o)
	}
	w.SetContent(container.NewGridWithColumns(3,
		cell(text), cell(rect), cell(circle),
		cell(line), cell(gradient), cell(radial),
		cell(checker), cell(img), cell(container.NewCenter(button)),
	))
	bellui.ShowAndRun(w)
}

Changing a primitive

The fields of a primitive are plain struct fields. After you change one, call Refresh() on the object so it is drawn again. Do both on the UI thread; the clock example on that page changes a canvas.Text this way.

Widgets are canvas objects

A widget is a canvas object with behaviour. The core.Widget interface adds one method to CanvasObject: CreateRenderer(). The renderer it returns holds the primitives that draw the widget, and lays them out. That is why a widget can sit next to a rectangle in any container:

// A widget is a canvas object too, so it goes anywhere the others go.
button := widget.NewButton("widget.Button", func() {})

Widgets that respond to input also implement interfaces from the event package; see Input events. To write your own widget, see Custom widgets.

Containers are canvas objects

A bellui.Container holds a list of child objects and a layout that positions them. It is a canvas object too, so containers nest. See Layout.