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) {
	defer clip.Rect{Max: image.Pt(100, 100)}.Push(ops).Pop()
	paint.ColorOp{Color: color.NRGBA{R: 0x80, A: 0xFF}}.Add(ops)

The defered line is only deferring the .Pop() operation at the end, so we push a rectangular clipping area, set the color to red with paint.ColorOp, and then instruct Gio to paint the current area with the current color with paint.PaintOp.


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) {
	defer op.Offset(image.Pt(100, 0)).Push(ops).Pop()

Again, note that we are defering the .Pop() of the offset. This means that the offset is applied for the duration of the function, then removed.


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 := image.Rect(0, 0, 100, 100)
	clip.RRect{Rect: bounds, SE: r, SW: r, NW: r, NE: r}.Push(ops)

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.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))
	defer clip.Outline{Path: path.End()}.Op().Push(ops).Pop()


To draw lines we can use clip.Stroke instead of clip.Outline. Stroke draws a fixed-width line along a path, whereas Outline simply does not allow drawing outside of the described path area. We can also use the paint.FillShape helper to avoid managing the clip state or use ColorOp or PaintOp. paint.FillShape lets us specify an *op.Ops, a color.NRGBA, and a clip.AreaOp, and it takes care of filling the clipped area with the color.

It’s possible to use the predefined shapes, such as clip.RRect:

func strokeRect(ops *op.Ops) {
	const r = 10
	bounds := image.Rect(20, 20, 80, 80)
	rrect := clip.RRect{Rect: bounds, SE: r, SW: r, NW: r, NE: r}
	paint.FillShape(ops, red,
			Path:  rrect.Path(ops),
			Width: 4,

Or use a custom shape drawn with clip.Path:

func strokeTriangle(ops *op.Ops) {
	var path clip.Path
	path.MoveTo(f32.Pt(30, 30))
	path.LineTo(f32.Pt(70, 30))
	path.LineTo(f32.Pt(50, 70))

	paint.FillShape(ops, green,
			Path:  path.End(),
			Width: 4,

For dashes, stroke end caps and joins, there’s a separate package gioui.org/x/stroke. However, they are not as performant as clip.Stroke, as the work to construct the path description must be performed on the CPU.

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 := image.Rect(0, 0, 100, 100)
	defer clip.RRect{Rect: bounds, SE: r, SW: r, NW: r, NE: r}.Push(ops).Pop()

Drawing Order

Drawing happens from back to front. Things inserted into the op.Ops first are drawn first, and later elements will be drawn on top. 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)

	// 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)

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)
	c := macro.Stop()

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


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.InvalidateCmd.

The following code will animate a red “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, source input.Source, 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.
	} 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)

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)
	call := macro.Stop()

	// Draw the operations from the cache.

Note: For this cache to actually save any work across frames, you’ll need to allocate the cache’s op.Ops somewhere that persists across frames. Doing it in a local variable like this will mean that the cache is recreated every frame.


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.Filter = paint.FilterNearest
	op.Affine(f32.Affine2D{}.Scale(f32.Pt(0, 0), f32.Pt(4, 4))).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. Additionally, mutations to the image provided to paint.ImageOp are not guaranteed to ever be reflected in the drawn content. To update an image on-screen, create a new image.Image and construct a new paint.ImageOp.