Drawing
Displaying things on the screen

The paint package provides operations for drawing graphics.

Coordinates are based on the top-left corner, although it’s possible to transform the coordinate system. This means f32.Point{X:0, Y:0} is the top left corner of the window. All drawing operations use pixel units, see Units section for more information.

For example, the following code will draw a 100x100 pixel colored rectangle at the top left corner of the window:

func drawRedRect(ops *op.Ops) {
	clip.Rect{Max: image.Pt(100, 100)}.Push(ops)
	paint.ColorOp{Color: color.NRGBA{R: 0x80, A: 0xFF}}.Add(ops)
	paint.PaintOp{}.Add(ops)
}


Offset

Operation op.TransformOp translates the position of the operations that come after it.

For example, the following function offsets the red rectangle 100 pixels to the right:

func drawRedRect10PixelsRight(ops *op.Ops) {
	op.Offset(f32.Pt(100, 0)).Add(ops)
	drawRedRect(ops)
}


Clipping

In some cases we want the drawing to be confined to a non-rectangular shape, for example to avoid overlapping drawings. Package gioui.org/op/clip provides exactly that.

clip.RRect clips all subsequent drawing operations to a rectangle with rounded corners. This is useful as a basis for a button background:

func redButtonBackground(ops *op.Ops) {
	const r = 10 // roundness
	bounds := f32.Rect(0, 0, 100, 100)
	clip.RRect{Rect: bounds, SE: r, SW: r, NW: r, NE: r}.Push(ops)
	drawRedRect(ops)
}


Note: that we first need to get the actual operation for the clipping with Op before calling Add. This level of indirection is useful if we want to use the same clipping operation multiple times. Under the hood, Op records a macro that encodes the clipping path.

For more complex clipping clip.Path can express shapes built from lines and bézier curves. This example draws a triangle with a curved edge:

func redTriangle(ops *op.Ops) {
	var path clip.Path
	path.Begin(ops)
	path.Move(f32.Pt(50, 0))
	path.Quad(f32.Pt(0, 90), f32.Pt(50, 100))
	path.Line(f32.Pt(-100, 0))
	path.Line(f32.Pt(50, -100))
	clip.Outline{Path: path.End()}.Op().Push(ops)
	drawRedRect(ops)
}


Operation Stack

Some operations affect all operations that follow them. For example, paint.ColorOp sets the “brush” color that is used in subsequent paint.PaintOp operations. This drawing context also includes coordinate transformation (set by op.TransformOp) and clipping (set by clip.Op).

Some operations, such as clips and transformations, allow you to temporarily apply them and later restore the previous state.

For example, the redButtonBackground function in the previous section has the unfortunate side-effect of clipping all later operations to the outline of the button background! Let’s make a version of it that doesn’t affect any callers:

func redButtonBackgroundStack(ops *op.Ops) {
	const r = 1 // roundness
	bounds := f32.Rect(0, 0, 100, 100)
	cl := clip.RRect{Rect: bounds, SE: r, SW: r, NW: r, NE: r}.Push(ops)
	drawRedRect(ops)
	cl.Pop()
}


Drawing Order

Drawing happens from back to front. In this function the green rectangle is drawn on top of red rectangle:

func drawOverlappingRectangles(ops *op.Ops) {
	// Draw a red rectangle.
	cl := clip.Rect{Max: image.Pt(100, 50)}.Push(ops)
	paint.ColorOp{Color: color.NRGBA{R: 0x80, A: 0xFF}}.Add(ops)
	paint.PaintOp{}.Add(ops)
	cl.Pop()

	// Draw a green rectangle.
	cl = clip.Rect{Max: image.Pt(50, 100)}.Push(ops)
	paint.ColorOp{Color: color.NRGBA{G: 0x80, A: 0xFF}}.Add(ops)
	paint.PaintOp{}.Add(ops)
	cl.Pop()
}


Sometimes you may want to change this order. For example, you may want to delay drawing to apply a transform that is calculated during drawing, or you may want to perform a list of operations several times. For this purpose there is op.MacroOp.

func drawFiveRectangles(ops *op.Ops) {
	// Record drawRedRect operations into the macro.
	macro := op.Record(ops)
	drawRedRect(ops)
	c := macro.Stop()

	// “Play back” the macro 5 times, each time
	// translated vertically 20px and horizontally 110 pixels.
	for i := 0; i < 5; i++ {
		c.Add(ops)
		op.Offset(f32.Pt(110, 20)).Add(ops)
	}
}


Animation

Gio only issues FrameEvents when the window is resized or the user interacts with the window. However, animation requires continuous redrawing until the animation is completed. For that there is op.InvalidateOp.

The following code will animate a green “progress bar” that fills up from left to right over 10 seconds from when the program starts:

var startTime = time.Now()
var duration = 10 * time.Second

func drawProgressBar(ops *op.Ops, now time.Time) {
	// Calculate how much of the progress bar to draw,
	// based on the current time.
	elapsed := now.Sub(startTime)
	progress := elapsed.Seconds() / duration.Seconds()
	if progress < 1 {
		// The progress bar hasn’t yet finished animating.
		op.InvalidateOp{}.Add(ops)
	} else {
		progress = 1
	}

	width := 200 * float32(progress)
	defer clip.Rect{Max: image.Pt(int(width), 20)}.Push(ops).Pop()
	paint.ColorOp{Color: color.NRGBA{R: 0x80, A: 0xFF}}.Add(ops)
	paint.PaintOp{}.Add(ops)
}


Record and replay

While op.MacroOp allows you to record and replay operations on a single operation list, op.CallOp allows for reuse of a separate operation list. This is useful for caching operations that are expensive to re-create, or for animating the disappearance of otherwise removed widgets:

func drawWithCache(ops *op.Ops) {
	// Save the operations in an independent ops value (the cache).
	cache := new(op.Ops)
	macro := op.Record(cache)

	cl := clip.Rect{Max: image.Pt(100, 100)}.Push(cache)
	paint.ColorOp{Color: color.NRGBA{G: 0x80, A: 0xFF}}.Add(cache)
	paint.PaintOp{}.Add(cache)
	cl.Pop()
	call := macro.Stop()

	// Draw the operations from the cache.
	call.Add(ops)
}


Images

paint.ImageOp is used to draw images. Like paint.ColorOp, it sets part of the drawing context (the “brush”) that’s used for subsequent PaintOp. ImageOp is used similarly to ColorOp.

Note that image.NRGBA and image.Uniform images are efficient and treated specially. Other Image implementations will undergo a more expensive copy and conversion to the underlying image model.

func drawImage(ops *op.Ops, img image.Image) {
	imageOp := paint.NewImageOp(img)
	imageOp.Add(ops)
	op.Affine(f32.Affine2D{}.Scale(f32.Pt(0, 0), f32.Pt(4, 4)))
	paint.PaintOp{}.Add(ops)
}


The image must not be mutated until another FrameEvent happens, because the image may be read asynchronously while the frame is being drawn.