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:
| Method | Meaning |
|---|---|
MinSize() geo.Size | The 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)| Type | Created with | Notes |
|---|---|---|
Text | NewText(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. |
Rectangle | NewRectangle(color) | FillColor, StrokeColor, StrokeWidth, and CornerRadius (or one radius per corner). |
Circle | NewCircle(color) | Fills its bounds. FillColor, StrokeColor, StrokeWidth. |
Line | NewLine(color) | Runs from Position1 to Position2. StrokeColor, StrokeWidth. |
LinearGradient | NewHorizontalGradient, NewVerticalGradient, NewLinearGradient(start, end, angle) | Set Stops for more than two colours; it then replaces the start and end colours. |
RadialGradient | NewRadialGradient(start, end) | From the centre outwards. |
Image | NewImageFromResource, NewImageFromFile, NewImageFromURI, NewImageFromReader, NewImageFromImage | Bitmap or SVG. See Resources and images. |
Raster | NewRaster, NewRasterWithPixels, NewRasterFromImage | Pixels 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.ImageFillContainThe 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.