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
Source
. - 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 window app.Window
window.Option(app.Title(title))
var ops op.Ops
for {
switch e := window.Event().(type) {
case app.DestroyEvent:
// The window was closed.
return e.Err
case app.FrameEvent:
// Reset the layout.Context for a new frame.
gtx := app.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 {
// Confine the area for pointer events.
area := clip.Rect(image.Rect(0, 0, 100, 100)).Push(gtx.Ops)
event.Op(gtx.Ops, b)
// here we loop through all the events associated with this button.
for {
ev, ok := gtx.Event(pointer.Filter{
Target: b,
Kinds: pointer.Press | pointer.Release,
})
if !ok {
break
}
e, ok := ev.(pointer.Event)
if !ok {
continue
}
switch e.Kind {
case pointer.Press:
b.pressed = true
case pointer.Release:
b.pressed = false
}
}
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)
}