Widget
Reusable and composable parts

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

  • They get input from an Queue.
  • They might hold some state.
  • They calculate their size given constraints.
  • They draw themselves to an op.Ops list.

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:

  • Constraints are an “incoming” parameter to a widget. The constraints hold a widget’s maximum (and minimum) size.
  • Ops holds the generated draw operations.
  • Events holds events generated since the last drawing operation.

By convention, functions that accept a layout.Context return layout.Dimensions which provides both the dimensions of the laid-out widget and the baseline of any text content within that widget.

var ops op.Ops
window := app.NewWindow()
for e := range 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)

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

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

Custom

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.NRGBA{R: 0x80, A: 0xFF}
	if b.pressed {
		col = color.NRGBA{G: 0x80, A: 0xFF}
	}
	return drawSquare(gtx.Ops, col)
}

func drawSquare(ops *op.Ops, color color.NRGBA) layout.Dimensions {
	defer clip.Rect{Max: image.Pt(100, 100)}.Push(ops).Pop()
	paint.ColorOp{Color: color}.Add(ops)
	paint.PaintOp{}.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 {
	// 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.
	area := clip.Rect(image.Rect(0, 0, 100, 100)).Push(gtx.Ops)
	pointer.InputOp{
		Tag:   b,
		Types: pointer.Press | pointer.Release,
	}.Add(gtx.Ops)
	area.Pop()

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