Introduction

Gio is a library for implementing immediate mode user interfaces. This approach can be implemented in multiple ways, however the overarching similarity is that the program:

  1. Listens for events such as mouse or keyboard input.
  2. Updates its internal state based on the event.
  3. Runs code that lays out and redraws the user interface state.

A minimal immediate mode command-line UI in pseudo-code:

main() {
	checked = false
	for every keypress {
		clear screen
		layoutCheckbox(keypress, &checked)
		if checked {
			print("info")
		}
	}
}

layoutCheckbox(keypress, checked) {
	if keypress == SPACE {
		*checked = !*checked
	}

	if *checked {
		print("[x]")
	} else {
		print("[ ]")
	}
}

In the immediate mode model, the program is in control of clearing and updating the display, and directly draws widgets and handle input during the updates.

In contrast, traditional “retained mode” libraries own the widgets through implicit library-managed state, typically arranged in a tree-like structure such as a browser’s DOM. As a result, the program must use the facilities given by the library to manipulate its widgets.

Actual GUI programming has several concerns in addition to the simple example above:

  1. How to get the events?
  2. When to redraw the state?
  3. What do the widget structures look like?
  4. How to track the focus?
  5. How to structure the events?
  6. How to communicate with the graphics card?
  7. How to handle input?
  8. How to draw text?
  9. Where does the widget state belong?
  10. And many more.

The rest of this document tries to answer how Gio does it. If you wish to know more about immediate mode UI, these references are a good start:

Window

Since a GUI library needs to talk to some sort of display system to display information:

window := app.NewWindow()
for {
	select {
	case e := <-window.Events():
		switch e := e.(type) {
		case system.DestroyEvent:
			// The window was closed.
			return e.Err
		case system.FrameEvent:
			// A request to draw the window state.
			ops := new(op.Ops)
			// Draw the state into ops.
			draw(ops)
			// Update the display.
			e.Frame(ops)
		}
	}
}

app.NewWindow chooses the appropriate “driver” depending on the environment and build context. It might choose Wayland, Win32, Cocoa among several others.

An app.Window sends events from the display system to the windows.Events() channel. The system events are listed in gioui.org/io/system. The input events, such as gioui.org/io/pointer and gioui.org/io/key, are also sent into that channel.

Operations

All UI libraries need a way for the program to specify what to display and how to handle events. Gio programs use operations, serialized into one or more op.Ops operation lists. Operation lists are in turn passed to the window driver through the FrameEvent.Frame function.

By convention, each operation kind is represented by a Go type with an Add method that records the operation into the Ops argument. Like any Go struct literal, zero-valued fields can be useful to represent optional values.

For example, recording an operation that sets the current color to red:

func addColorOperation(ops *op.Ops) {
	red := color.RGBA{R: 0xFF, A: 0xFF}
	paint.ColorOp{Color: red}.Add(ops)
}

You might be thinking that it would be more usual to have an ops.Add(ColorOp{Color: red}) method instead of using op.ColorOp{Color: red}.Add(ops). It’s like this so that the Add method doesn’t have to take an interface-typed argument, which would often require an allocation to call. This is a key aspect of Gio’s “zero allocation” design.

Drawing

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 10x10 pixel colored rectangle at the top level corner of the window:

func drawRedRect(ops *op.Ops) {
	paint.ColorOp{Color: color.RGBA{R: 0x80, A: 0xFF}}.Add(ops)
	paint.PaintOp{Rect: f32.Rect(0, 0, 100, 100)}.Add(ops)
}


Transformation

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.TransformOp{}.Offset(f32.Pt(100, 0)).Add(ops)
	drawRedRect(ops)
}


Note: in the future, TransformOp will allow other transformations such as scaling and rotation.

Clipping

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

clip.Rect 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.Rect{Rect: bounds, SE: r, SW: r, NW: r, NE: r}.Add(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))
	path.End().Add(ops)
	drawRedRect(ops)
}


Push and Pop

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

We often need to set up some drawing context and then restore it to its previous state, leaving later operations unaffected. We can use op.StackOp to do this. A Push operation saves the current drawing context; a Pop operation restores it.

For example, the clipButtonOutline 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) {
	var stack op.StackOp
	stack.Push(ops)
	defer stack.Pop()

	const r = 1 // roundness
	bounds := f32.Rect(0, 0, 100, 100)
	clip.Rect{Rect: bounds, SE: r, SW: r, NW: r, NE: r}.Op(ops).Add(ops)
	drawRedRect(ops)
}


Drawing Order and Macros

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.
	paint.ColorOp{Color: color.RGBA{R: 0x80, A: 0xFF}}.Add(ops)
	paint.PaintOp{Rect: f32.Rect(0, 0, 100, 50)}.Add(ops)

	// Draw a green rectangle.
	paint.ColorOp{Color: color.RGBA{G: 0x80, A: 0xFF}}.Add(ops)
	paint.PaintOp{Rect: f32.Rect(0, 0, 50, 100)}.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.
	var macro op.MacroOp
	macro.Record(ops)
	drawRedRect(ops)
	macro.Stop()

	// “Play back” the macro 5 times, each time
	// translated vertically 20px and horizontally 110 pixels.
	for i := 0; i < 5; i++ {
		macro.Add()
		op.TransformOp{}.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 5 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
	}

	paint.ColorOp{Color: color.RGBA{G: 0x80, A: 0xFF}}.Add(ops)
	width := 200 * float32(progress)
	paint.PaintOp{Rect: f32.Rect(0, 0, width, 20)}.Add(ops)
}


Reusing operations with CallOp

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)
	paint.ColorOp{Color: color.RGBA{G: 0x80, A: 0xFF}}.Add(cache)
	paint.PaintOp{Rect: f32.Rect(0, 0, 100, 100)}.Add(cache)

	// Draw the operations from the cache.
	op.CallOp{Ops: cache}.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.RGBA 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)
	paint.PaintOp{Rect: f32.Rect(0, 0, 100, 100)}.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.

Fonts

gioui.org/font contains the central registry for fonts. This helps to reduce passing fonts around throughout the program.

There is one font bundled in package gioui.org/font/gofont, you can use gofont.Register() to register it to the global registry.

For loading other fonts there is gioui.org/font/opentype. After parsing the font(s) using opentype.Parse or opentype.ParseCollection they can be registered with font.Register.

Text

For converting strings to clip shapes there is the gioui.org/text package.

It contains text.FontRegistry that implements cached string to shape conversion, with appropriate fallbacks.

In most cases you can use widget.Label which handles wrapping and layout constraints. Or when you are using material design then material.LabelStyle.

Input

Input is delivered to the widgets via a system.FrameEvent through the Queue field.

Some of the most common events in FrameEvent.Queue are:

The program can respond to these events however it likes - for example, by updating its local data structures or running a user-triggered action. The FrameEvent is special - when the program receives a FrameEvent, it is responsible for updating the display by calling the e.Frame function with an operation list representing the new state. These operations are generated immediately in response to the FrameEvent which is the main reason that Gio is known as an “immediate mode” GUI.

Event-processors, such as Click and Scroll from package gioui.org/gesture detect higher-level actions from individual click events.

To distribute input among multiple different widgets, Gio needs to know about event handlers and their configuration. However, since the Gio framework is stateless, there’s no direct way for the program to specify that.

Instead, some operations associate input event types (for example, keyboard presses) with arbitrary tags (interface{} values) chosen by the program. A program creates these operations when it’s processing the FrameEvent- input operations are operations like any other. In return, an event.Queue supplies the events that arrived since the last frame, separated by tag.

The following example demonstrates pointer input handling:

var tag = new(bool) // We could use &pressed for this instead.
var pressed = false

func doButton(ops *op.Ops, q event.Queue) {
	// Make sure we don’t pollute the graphics context.
	var stack op.StackOp
	stack.Push(ops)
	defer stack.Pop()

	// Process events that arrived between the last frame and this one.
	for _, ev := range q.Events(tag) {
		if x, ok := ev.(pointer.Event); ok {
			switch x.Type {
			case pointer.Press:
				pressed = true
			case pointer.Release:
				pressed = false
			}
		}
	}

	// Confine the area of interest to a 100x100 rectangle.
	pointer.Rect(image.Rect(0, 0, 100, 100)).Add(ops)
	// Declare the tag.
	pointer.InputOp{Tag: tag}.Add(ops)

	var c color.RGBA
	if pressed {
		c = color.RGBA{R: 0xFF, A: 0xFF}
	} else {
		c = color.RGBA{G: 0xFF, A: 0xFF}
	}
	paint.ColorOp{Color: c}.Add(ops)
	paint.PaintOp{Rect: f32.Rect(0, 0, 100, 100)}.Add(ops)
}


It’s convenient to use a Go pointer value for the input tag, as it’s cheap to convert a pointer to an interface{} and it’s easy to make the value specific to a local data structure, which avoids the risk of tag conflict.

For more details take a look at gioui.org/io/pointer (pointer/mouse events) and gioui.org/io/key (keyboard events).

Handling external state changes

A single frame consists of getting input, registering for input and drawing the new state:

window := app.NewWindow()
for {
	select {
	case e := <-window.Events():
		switch e := e.(type) {
		case system.DestroyEvent:
			// The window was closed.
			return e.Err
		case system.FrameEvent:
			// A request to draw the window state.
			ops := new(op.Ops)
			// Draw the state into ops based on events in e.Queue.
			draw(ops, e.Queue)
			// Update the display.
			e.Frame(ops)
		}
	}
}

Let’s make the button change it’s position every second. We can use a select to wait for events from the window and the external source at the same time. We’ll use a Ticker as an example external change. Once we have modified the state we need to notify the window to retrigger rendering with w.Invalidate().

window := app.NewWindow()

changes := time.NewTicker(time.Second)
defer changes.Stop()

buttonOffset := float32(0.0)

ops := new(op.Ops)
for {
	select {
	case e := <-window.Events():
		switch e := e.(type) {
		case system.DestroyEvent:
			return e.Err
		case system.FrameEvent:
			ops.Reset()

			// Offset the button based on state.
			op.TransformOp{}.Offset(f32.Pt(buttonOffset, 0)).Add(ops)

			// Handle button input and draw.
			doButton(ops, e.Queue)

			// Update display.
			e.Frame(ops)
		}

	case t := <-changes.C:
		buttonOffset = float32(t.Second()%3) * 100
		window.Invalidate()
	}
}


Writing a program using these concepts could get really verbose, which is why Gio provides standard widgets for common look and behaviour. Most programs end up using widgets primarily and few low-level operations.

Widget

We’ve been mentioning widgets quite a while now. In principle widgets are composable and drawable UI elements that may react to input. Or to put more concretely.

By convention, widgets have a Layout method that does all of the above. Some widgets have separate methods for querying their state or to pass events back to the program.

Some widgets have several visual representations. For example, the stateful Clickable is used as basis for buttons and icon buttons. In fact, the material package implements only the Material Design and is intended to be supplemented by other packages implementing different designs.

Context

To build out more complex UI from these primitives we need some structure that describes the layout in a composable way.

It’s possible to specify a layout statically, but display sizes vary greatly, so we need to be able to calculate the layout dynamically - that is constrain the available display size and then calculate the rest of the layout. We also need a comfortable way of passing events through the composed structure and similarly we need a way to pass op.Ops through the system.

layout.Context conveniently bundles these aspects together. It carries the state that is needed by almost all layouts and widgets.

To summarise the terminology:

var ops op.Ops
window := app.NewWindow()
for {
	select {
	case e := <-window.Events():
		switch e := e.(type) {
		case system.DestroyEvent:
			// The window was closed.
			return e.Err
		case system.FrameEvent:
			// Reset the layout.Context for a new frame.
			gtx := layout.NewContext(&ops, e.Queue, e.Config, e.Size)

			// Draw the state into ops based on events in e.Queue.
			draw(gtx)

			// Update the display.
			e.Frame(gtx.Ops)
		}
	}
}

Units

Drawing operations use pixel coordinates, ignoring any transformation applied. However, for most use-cases you don’t want to tie your user-interface sizes and positions to screen pixels. People may have screen-scaling enabled and pixel densities vary significantly between devices.

In addition to the physical pixel, package gioui.org/unit implements device independent units:

layout.Context has method Px to convert from unit.Value to pixels

For more information on pixel-density see:

Coordinate systems

You may have noticed that widget constraints and dimensions sizes are in integer units, while drawing commands such as PaintOp use floating point units. That’s because they refer to two distinct coordinate systems, the layout coordinate system and the drawing coordinate system. The distinction is subtle, but important.

The layout coordinate system is in integer pixels, because it’s important that widgets never unintentionally overlap in the middle of a physical pixel. In fact, the decision to use integer coordinates was motivated by conflation issues in other UI libraries caused by allowing fractional layouts.

As a bonus, integer coordinates are perfectly deterministic across all platforms which leads to easier debugging and testing of layouts.

On the other hand, drawing commands need the generality of floating point coordinates for smooth animation and for expression inherently fractional shapes such as bézier curves.

It’s possible to draw shapes that overlap at fractional pixel coordinates, but only intentionally: drawing commands directly derived from layout constraints have integer coordinates by construction.

Custom Widget

As an example, here is how to implement a very simple button.

Let’s start by drawing it:

type ButtonVisual struct {
	pressed bool
}

func (b *ButtonVisual) Layout(gtx layout.Context) layout.Dimensions {
	col := color.RGBA{R: 0x80, A: 0xFF}
	if b.pressed {
		col = color.RGBA{G: 0x80, A: 0xFF}
	}
	return drawSquare(gtx.Ops, col)
}

func drawSquare(ops *op.Ops, color color.RGBA) layout.Dimensions {
	square := f32.Rect(0, 0, 100, 100)
	paint.ColorOp{Color: color}.Add(ops)
	paint.PaintOp{Rect: square}.Add(ops)
	return layout.Dimensions{Size: image.Pt(100, 100)}
}


Then handle pointer clicks:

type Button struct {
	pressed bool
}

func (b *Button) Layout(gtx layout.Context) layout.Dimensions {
	// Avoid affecting the input tree with pointer events.
	var stack op.StackOp
	stack.Push(gtx.Ops)
	defer stack.Pop()

	// here we loop through all the events associated with this button.
	for _, e := range gtx.Events(b) {
		if e, ok := e.(pointer.Event); ok {
			switch e.Type {
			case pointer.Press:
				b.pressed = true
			case pointer.Release:
				b.pressed = false
			}
		}
	}

	// Confine the area for pointer events.
	pointer.Rect(image.Rect(0, 0, 100, 100)).Add(gtx.Ops)
	pointer.InputOp{Tag: b}.Add(gtx.Ops)

	// Draw the button.
	col := color.RGBA{R: 0x80, A: 0xFF}
	if b.pressed {
		col = color.RGBA{G: 0x80, A: 0xFF}
	}
	return drawSquare(gtx.Ops, col)
}


Layout

Package gioui.org/layout provides support for common layout operations such as spacing, lists and stacks.

In the layout examples we’ll use this ColorBox widget to visualize layouts:

// Test colors.
var (
	background = color.RGBA{R: 0xC0, G: 0xC0, B: 0xC0, A: 0xFF}
	red        = color.RGBA{R: 0xC0, G: 0x40, B: 0x40, A: 0xFF}
	green      = color.RGBA{R: 0x40, G: 0xC0, B: 0x40, A: 0xFF}
	blue       = color.RGBA{R: 0x40, G: 0x40, B: 0xC0, A: 0xFF}
)

// ColorBox creates a widget with the specified dimensions and color.
func ColorBox(gtx layout.Context, size image.Point, color color.RGBA) layout.Dimensions {
	bounds := f32.Rect(0, 0, float32(size.X), float32(size.Y))
	paint.ColorOp{Color: color}.Add(gtx.Ops)
	paint.PaintOp{Rect: bounds}.Add(gtx.Ops)
	return layout.Dimensions{Size: size}
}

Inset

layout.Inset adds space around a widget.

func inset(gtx layout.Context) layout.Dimensions {
	// Draw rectangles inside of each other, with 20dp padding.
	return layout.UniformInset(unit.Dp(30)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
		return ColorBox(gtx, gtx.Constraints.Max, red)
	})
}


Stack

layout.Stack lays out child elements on top of each other, according to the alignment direction. The child of a stack layout can be:

For example, this draws green and blue rectangles on top of a red background:

func stacked(gtx layout.Context) layout.Dimensions {
	return layout.Stack{}.Layout(gtx,
		// Force widget to the same size as the second.
		layout.Expanded(func(gtx layout.Context) layout.Dimensions {
			// This will have a minimum constraint of 100x100.
			return ColorBox(gtx, gtx.Constraints.Min, red)
		}),
		layout.Stacked(func(gtx layout.Context) layout.Dimensions {
			return ColorBox(gtx, image.Pt(100, 30), green)
		}),
		layout.Stacked(func(gtx layout.Context) layout.Dimensions {
			return ColorBox(gtx, image.Pt(30, 100), blue)
		}),
	)
}


List

layout.List can display a potentially large list of items. Since List also handles scrolling it must be persisted across layouts, otherwise the scrolling position is lost.

var list = layout.List{}

func listing(gtx layout.Context) layout.Dimensions {
	return list.Layout(gtx, 100, func(gtx layout.Context, i int) layout.Dimensions {
		col := color.RGBA{R: byte(i * 20), G: 0x20, B: 0x20, A: 0xFF}
		return ColorBox(gtx, image.Pt(20, 100), col)
	})
}


Flex

layout.Flex lays out children according to their weights or rigid constraints. First the rigid elements are used to determine the remaining space and then the remaining space is divided among flexed children according to weights.

The children can be:

func flexed(gtx layout.Context) layout.Dimensions {
	return layout.Flex{}.Layout(gtx,
		layout.Rigid(func(gtx layout.Context) layout.Dimensions {
			return ColorBox(gtx, image.Pt(100, 100), red)
		}),
		layout.Flexed(0.5, func(gtx layout.Context) layout.Dimensions {
			return ColorBox(gtx, gtx.Constraints.Min, blue)
		}),
		layout.Rigid(func(gtx layout.Context) layout.Dimensions {
			return ColorBox(gtx, image.Pt(100, 100), red)
		}),
		layout.Flexed(0.5, func(gtx layout.Context) layout.Dimensions {
			return ColorBox(gtx, gtx.Constraints.Min, green)
		}),
	)
}


Themes

The same abstract widget can have many visual representations, ranging from simple color changes to entirely custom graphics. To give an application a consistent appearance it is useful to have an abstraction that represents a particular “theme”.

Package gioui.org/widget/material implements a theme based on the Material Design, and the Theme struct encapsulates the parameters for varying colors, sizes and fonts.

To use a theme, you must first initialize in your application loop:

gofont.Register()
th := material.NewTheme()

var ops op.Ops
window := app.NewWindow()
for {
	select {
	case e := <-window.Events():
		switch e := e.(type) {
		case system.DestroyEvent:
			// The window was closed.
			return e.Err
		case system.FrameEvent:
			// Reset the layout.Context for a new frame.
			gtx := layout.NewContext(&ops, e.Queue, e.Config, e.Size)

			// Draw the state into ops based on events in e.Queue.
			draw(gtx, th)

			// Update the display.
			e.Frame(gtx.Ops)
		}
	}
}

Then in your application use the provided widgets:

var isChecked widget.Bool

func themedApplication(gtx layout.Context, th *material.Theme) layout.Dimensions {
	var checkboxLabel string
	if isChecked.Value {
		checkboxLabel = "checked"
	} else {
		checkboxLabel = "not-checked"
	}

	return layout.Flex{
		Axis: layout.Vertical,
	}.Layout(gtx,
		layout.Rigid(material.H3(th, "Hello, World!").Layout),
		layout.Rigid(material.CheckBox(th, &isChecked, checkboxLabel).Layout),
	)
}


Kitchen example shows all the different widgets available.

Custom Layout

Sometimes the builtin layouts are not sufficient. To create a custom layout for widgets there are special functions and structures to manipulate layout.Context. In general, layouting code performs the following steps for each sub-widget:

For complicated layouts you would also need to use macros. As an example take a look at layout.Flex. Which roughly implements:

  1. Record widgets in macros.
  2. Calculate sizes for non-rigid widgets.
  3. Draw widgets based on the calculated sizes by replaying their macros.

Example: Split View Widget

As an example, the following layout displays two widgets side-by-side:

type SplitVisual struct{}

func (s SplitVisual) Layout(gtx layout.Context, left, right layout.Widget) layout.Dimensions {
	leftsize := gtx.Constraints.Min.X / 2
	rightsize := gtx.Constraints.Min.X - leftsize

	{
		var stack op.StackOp
		stack.Push(gtx.Ops)

		gtx := gtx
		gtx.Constraints = layout.Exact(image.Pt(leftsize, gtx.Constraints.Max.Y))
		left(gtx)

		stack.Pop()
	}

	{
		var stack op.StackOp
		stack.Push(gtx.Ops)

		gtx := gtx
		gtx.Constraints = layout.Exact(image.Pt(rightsize, gtx.Constraints.Max.Y))
		op.TransformOp{}.Offset(f32.Pt(float32(leftsize), 0)).Add(gtx.Ops)
		right(gtx)

		stack.Pop()
	}

	return layout.Dimensions{Size: gtx.Constraints.Max}
}

The usage code would look like:

func exampleSplitVisual(gtx layout.Context, th *material.Theme) layout.Dimensions {
	return SplitVisual{}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
		return FillWithLabel(gtx, th, "Left", red)
	}, func(gtx layout.Context) layout.Dimensions {
		return FillWithLabel(gtx, th, "Right", blue)
	})
}

func FillWithLabel(gtx layout.Context, th *material.Theme, text string, backgroundColor color.RGBA) layout.Dimensions {
	ColorBox(gtx, gtx.Constraints.Max, backgroundColor)
	return layout.Center.Layout(gtx, material.H3(th, text).Layout)
}


Interactivity

To make it more useful we could make the split draggable.

First let’s make the ratio adjustable. We should try to make zero values useful, in this case 0 could mean that it’s split in the center.

type SplitRatio struct {
	// Ratio keeps the current layout.
	// 0 is center, -1 completely to the left, 1 completely to the right.
	Ratio float32
}

func (s SplitRatio) Layout(gtx layout.Context, left, right layout.Widget) layout.Dimensions {
	proportion := (s.Ratio + 1) / 2
	leftsize := int(proportion * float32(gtx.Constraints.Max.X))

	rightoffset := leftsize
	rightsize := gtx.Constraints.Max.X - rightoffset

	{
		var stack op.StackOp
		stack.Push(gtx.Ops)

		gtx := gtx
		gtx.Constraints = layout.Exact(image.Pt(leftsize, gtx.Constraints.Max.Y))
		left(gtx)

		stack.Pop()
	}

	{
		var stack op.StackOp
		stack.Push(gtx.Ops)

		op.TransformOp{}.Offset(f32.Pt(float32(rightoffset), 0)).Add(gtx.Ops)
		gtx := gtx
		gtx.Constraints = layout.Exact(image.Pt(rightsize, gtx.Constraints.Max.Y))
		right(gtx)

		stack.Pop()
	}

	return layout.Dimensions{Size: gtx.Constraints.Max}
}

The usage code would look like:

func exampleSplitRatio(gtx layout.Context, th *material.Theme) layout.Dimensions {
	return SplitRatio{Ratio: -0.3}.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
		return FillWithLabel(gtx, th, "Left", red)
	}, func(gtx layout.Context) layout.Dimensions {
		return FillWithLabel(gtx, th, "Right", blue)
	})
}


Because we also need to have an area designated for moving the split, let’s add a bar into the center:

bar := gtx.Px(s.Bar)
if bar <= 1 {
	bar = gtx.Px(defaultBarWidth)
}

proportion := (s.Ratio + 1) / 2
leftsize := int(proportion*float32(gtx.Constraints.Max.X) - float32(bar))

rightoffset := leftsize + bar
rightsize := gtx.Constraints.Max.X - rightoffset

Now we need to store our interactive state:

type Split struct {
	// Ratio keeps the current layout.
	// 0 is center, -1 completely to the left, 1 completely to the right.
	Ratio float32
	// Bar is the width for resizing the layout
	Bar unit.Value

	drag   bool
	dragID pointer.ID
	dragX  float32
}

And then we need to handle input events:

for _, ev := range gtx.Events(s) {
	e, ok := ev.(pointer.Event)
	if !ok {
		continue
	}

	switch e.Type {
	case pointer.Press:
		if s.drag {
			break
		}

		s.drag = true
		s.dragID = e.PointerID
		s.dragX = e.Position.X

	case pointer.Move:
		if !s.drag || s.dragID != e.PointerID {
			break
		}

		deltaX := e.Position.X - s.dragX
		s.dragX = e.Position.X

		deltaRatio := deltaX * 2 / float32(gtx.Constraints.Max.X)
		s.Ratio += deltaRatio

	case pointer.Release:
		fallthrough
	case pointer.Cancel:
		if !s.drag || s.dragID != e.PointerID {
			break
		}
		s.drag = false
	}
}

// register for input
barRect := image.Rect(leftsize, 0, rightoffset, gtx.Constraints.Max.X)
pointer.Rect(barRect).Add(gtx.Ops)
pointer.InputOp{Tag: s, Grab: s.drag}.Add(gtx.Ops)

Putting the whole widget together:

type Split struct {
	// Ratio keeps the current layout.
	// 0 is center, -1 completely to the left, 1 completely to the right.
	Ratio float32
	// Bar is the width for resizing the layout
	Bar unit.Value

	drag   bool
	dragID pointer.ID
	dragX  float32
}


var defaultBarWidth = unit.Dp(10)

func (s *Split) Layout(gtx layout.Context, left, right layout.Widget) layout.Dimensions {
	bar := gtx.Px(s.Bar)
	if bar <= 1 {
		bar = gtx.Px(defaultBarWidth)
	}

	proportion := (s.Ratio + 1) / 2
	leftsize := int(proportion*float32(gtx.Constraints.Max.X) - float32(bar))

	rightoffset := leftsize + bar
	rightsize := gtx.Constraints.Max.X - rightoffset

	{ // handle input
		// Avoid affecting the input tree with pointer events.
		var stack op.StackOp
		stack.Push(gtx.Ops)
		defer stack.Pop()

		for _, ev := range gtx.Events(s) {
			e, ok := ev.(pointer.Event)
			if !ok {
				continue
			}

			switch e.Type {
			case pointer.Press:
				if s.drag {
					break
				}

				s.drag = true
				s.dragID = e.PointerID
				s.dragX = e.Position.X

			case pointer.Move:
				if !s.drag || s.dragID != e.PointerID {
					break
				}

				deltaX := e.Position.X - s.dragX
				s.dragX = e.Position.X

				deltaRatio := deltaX * 2 / float32(gtx.Constraints.Max.X)
				s.Ratio += deltaRatio

			case pointer.Release:
				fallthrough
			case pointer.Cancel:
				if !s.drag || s.dragID != e.PointerID {
					break
				}
				s.drag = false
			}
		}

		// register for input
		barRect := image.Rect(leftsize, 0, rightoffset, gtx.Constraints.Max.X)
		pointer.Rect(barRect).Add(gtx.Ops)
		pointer.InputOp{Tag: s, Grab: s.drag}.Add(gtx.Ops)
	}

	{
		var stack op.StackOp
		stack.Push(gtx.Ops)

		gtx := gtx
		gtx.Constraints = layout.Exact(image.Pt(leftsize, gtx.Constraints.Max.Y))
		left(gtx)

		stack.Pop()
	}

	{
		var stack op.StackOp
		stack.Push(gtx.Ops)

		op.TransformOp{}.Offset(f32.Pt(float32(rightoffset), 0)).Add(gtx.Ops)
		gtx := gtx
		gtx.Constraints = layout.Exact(image.Pt(rightsize, gtx.Constraints.Max.Y))
		right(gtx)

		stack.Pop()
	}

	return layout.Dimensions{Size: gtx.Constraints.Max}
}

And an example:

var split Split

func exampleSplit(gtx layout.Context, th *material.Theme) layout.Dimensions {
	return split.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
		return FillWithLabel(gtx, th, "Left", red)
	}, func(gtx layout.Context) layout.Dimensions {
		return FillWithLabel(gtx, th, "Right", blue)
	})
}


Common errors

The system is drawing on top of my custom widget, or otherwise ignoring its size.

The problem: You’ve created a nice new widget. You lay it out, say, in a Flex Rigid. The next Rigid draws on top of it.

The explanation: Gio communicates the size of widgets dynamically via layout.Context.Dimensions (commonly “gtx.Dimensions”). High level widgets (such as Labels) “return” or pass on their dimensions in gtx.Dimensions, but lower-level operations, such as paint.PaintOp, do not set Dimensions.

The solution: Update gtx.Dimensions in your widget’s Layout function before you return.

My list.List won’t scroll

The problem: You lay out a list and then it just sits there and doesn’t scroll.

The explanation: A lot of widgets in Gio are context free – you can and should declare them every time through your Layout function. Lists are not like that. They record their scroll position internally, and that needs to persist between calls to Layout.

The solution: Declare your List once outside the event handling loop and reuse it across frames.

The system is ignoring updates to a widget

The problem: You define a field in your widget struct with the widget. You update the child widget state, either implicitly or explicitly. The child widget stubbornly refuses to reflect your updates.

This is related to the problem with Lists that won’t scroll.

One possible explanation: You might be seeing a common “gotcha” in Go code, where you’ve defined a method on a value receiver, not a pointer receiver, so all the updates you’re making to your widget are only visible inside that function, and thrown away when it returns.

The solution: Layout and Update methods on stateful widgets should have pointer receivers.