Written by Christina Forney for the GopherCon 2019 Liveblog
Presenter: Elias Naur
Liveblogger: Christina Forney
Gio is a new open source Go library for writing immediate mode GUI programs that run on all the major platforms: Android, iOS/tvOS, macOS, Linux, Windows. The talk will cover Gio's unusual design and how it achieves simplicity, portability and performance.
Last year at GopherCon we asked what the biggest challenges that Go developers faced. Here are the results:
As you heard in the keynote, Modules, generics, and error handling is being handled by the Go team, so I wanted to focus on making writing GUIs in Go easy.
Gio - gioui.org
Scatter - scatter.im
Scatter is a multi-platform messaging application for sending and receiving encrypted chat messages, implementing the Signal protocol over federated email.
I wanted to be able to write a GUI program in GO that I could implement only once and have it work on every platform. This, to me, is the most interesting feature of Gio.
Features:
Immediate mode design.
Only depends on lowest-level platform libraries.
GPU accelerated vector and text rendering.
Some programs require you to maintain state for your widgetry. In Gio, you draw what you need to draw, you layout what you need to layout, and that’s it!
This is all you need to render a simple blank window:
package main
import (
“gioui.org/ui/app”
)
func main() {
go func() {
w := app.NewWindow(nil)
for range w.Events() {
}
}()
app.Main()
}
This is odd, because you’re doing the event loop in your go routine.
Slightly more advanced example, but in this case you are loading up some support structures and adding text.Label
to display your label:
func main() {
go func() {
w := app.NewWindow(nil)
regular, _ := sfnt.Parse(goregular.TTF)
var cfg ui.Config
var faces measure.Faces
ops := new(ui.Ops)
for e := range w.Events() {
if e, ok := e.(app.DrawEvent); ok {
cfg = &e.Config
cs := layout.RigidConstraints(e.Size)
ops.Reset()
faces.Reset(cfg)
// ADD YOUR LABELS
lbl := text.Label{Face: faces.For(regular, ui.Sp(72)), Text: “Hello, World!”}
lbl.Layout(ops, cs)
w.Draw(ops)
}
}
}()
app.Main()
}
export GO111MODULE=on
I recommend you enable for convenience and also because I break the API often, so you will be shielded from updates that could break your application until you are ready to upgrade.
go build gioui.org/ui/apps/hello
go install scatter.im/cmd/scatter
go run helloworld.go
There is a tool to package your application as an APK that you can install through the ads tool to run on a device or simulator.
Install the gio tool:
go install gioui.org/cmd/gio
$GOBIN/gio -target android -o hello.apk helloworld.go
Install on a connected device or emulator with adb:
adb install hello.apk
For iOS/tvOS devices:
$GOBIN/gio -target <ios|tvos> -o hello.ipa -appid <bundle id> helloworld.go
Use the .app file extension for simulators:
$GOBIN/gio -target <ios|tvos> -o hello.app helloworld.go
Install on a running simulator:
xcrun simctl install booted hello.app
To output a directory ready to serve:
$GOBIN/gio -target js -o www helloworld.go
Use a webserver or goexec to serve it:
go run github.com/shurcooL/goexec ‘http.ListenAndServe(“:8080”, http.FileServer(http.Dir(“www”)))’
Compile directly with the Go tool or use the Gio tool to build as a web assembly module, but also add the necessary file to supply it to work in your browser.
The way you communicate each user interface update to gio. Gio has not state so you have to add it to every frame.
Operations buffer and type called ui ops and you add operations to that to your ops buffers which sends to window.draw method.
import “gioui.org/ui” // Pure Go
var ops ui.Ops
// Add operations to ops
ui.InvalidateOp{}.Add(ops)
…
import “gioui.org/ui/app”
var w app.Window
w.Draw(&ops)
import “gioui.org/ui”
ui.TransformOp{ui.Offset(f32.Point{…})}.Add(ops)
ui.InvalidateOp{}.Add(ops) // Immediate
ui.InvalidateOp{At: …}.Add(ops) // Delayed
import “gioui.org/ui/draw”
draw.ColorOp{Color: color.RGBA{…}}.Add(ops)
draw.ImageOp{Src: …, Rect: …}.Add(ops)
draw.DrawOp{Rect: …}.Add(ops)
import “gioui.org/draw”
draw.RectClip(image.Rectangle{…}).Add(ops)
var b draw.PathBuilder
b.Init(ops)
b.Line(…)
b.Quad(…) // Quadratic Beziér curve
b.Cube(…) // Cubic Beziér curve
b.End()
import “gioui.org/ui/key”
// Declare key handler.
key.HandlerOp{Key: handler, Focus: true/false}.Add(ops)
// Hide soft keyboard.
key.HideInputOp{}.Add(ops)
import “gioui.org/ui/pointer”
// Define hit area.
pointer.RectAreaOp{Size: …}.Add(ops)
pointer.EllipseAreaOp{Size: …}.Add(ops)
// Declare pointer handler.
pointer.HandlerOp{Key: c, Grab true/false}
square := f32.Rectangle{Max: f32.Point{X: 500, Y: 500}}
radius := animateRadius(e.Config.Now(), 250)
// Position
ui.TransformOp{ui.Offset(f32.Point{
X: 100,
Y: 100,
})}.Add(ops)
// Color
draw.ColorOp{Color: color.RGBA{A: 0xff, G: 0xcc}}.Add(ops)
// Clip corners
roundRect(ops, 500, 500, radius, radius, radius, radius)
// Draw
draw.DrawOp{Rect: square}.Add(ops)
// Animate
ui.InvalidateOp{}.Add(ops)
// Submit operations to the window.
w.Draw(ops)
If you have non-trivial setup, you need some way to lay them out - you don’t want to use absolute coordinates for each item. Layout assembly helps you structure your user interface. As a result of calling their layout, widgets will give you their own size.
package layout // import gioui.org/ui/layout
type Constraints struct {
Width Constraint
Height Constraint
}
type Constraint struct {
Min, Max int
}
type Dimens struct {
Size image.Point
Baseline int
}
package text // import gioui.org/ui/text
func (l Label) Layout(ops *ui.Ops, cs layout.Constraints) layout.Dimens
func (e *Editor) Layout(ops *ui.Ops, cs layout.Constraints) layout.Dimens
package widget // import gioui.org/ui/widget
func (im Image) Layout(c ui.Config, ops *ui.Ops, cs layout.Constraints) layout.Dimens
func drawLabels(face text.Face, ops *ui.Ops, cs layout.Constraints) {
**cs.Height.Min = 0**
lbl := text.Label{Face: face, Text: “One label”}
**dimensions := lbl.Layout(ops, cs)**
ui.TransformOp{ui.Offset(f32.Point{
**Y: float32(dimensions.Size.Y),**
})}.Add(ops)
lbl2 := text.Label{Face: face, Text: “Another label”}
lbl2.Layout(ops, cs)
}
Can layout to the compass directions or to specific place, like the center.
var ops *ui.Ops
var cs layout.Constraints
align := layout.Align{Alignment: layout.Center}
cs = align.Begin(ops, cs)
…
dimensions := someWidget.Layout(…, cs) // Draw widget
…
dimensions = align.End(dimensions)
var cfg ui.Config
inset := layout.Inset{Top: ui.Dp(8), …} // 8dp top inset
cs = inset.Begin(c, ops, cs)
…
dimensions := anotherWidget.Layout(…, cs) // Draw widget
…
dimensions = inset.End(dimensions)
func drawRects(c ui.Config, ops *ui.Ops, cs layout.Constraints) {
flex := layout.Flex{}
flex.Init(ops, cs)
cs = flex.Flexible(0.5)
dimensions := drawRect(c, ops, color.RGBA{A: 0xff, R: 0xff}, cs)
red := flex.End(dimensions)
cs = flex.Flexible(0.25)
dimensions = drawRect(c, ops, color.RGBA{A: 0xff, G: 0xff}, cs)
green := flex.End(dimensions)
cs = flex.Flexible(0.25)
dimensions = drawRect(c, ops, color.RGBA{A: 0xff, B: 0xff}, cs)
blue := flex.End(dimensions)
flex.Layout(red, green, blue)
}
func drawRects(c ui.Config, ops *ui.Ops, cs layout.Constraints) {
stack := layout.Stack{Alignment: layout.Center}
stack.Init(ops, cs)
cs = stack.Rigid()
dimensions := drawRect(c, ops, color.RGBA{A: 0xff, R: 0xff}, ui.Dp(50), cs)
red := stack.End(dimensions)
cs = stack.Rigid()
dimensions = drawRect(c, ops, color.RGBA{A: 0xff, G: 0xff}, ui.Dp(100), cs)
green := stack.End(dimensions)
cs = stack.Rigid()
dimensions = drawRect(c, ops, color.RGBA{A: 0xff, B: 0xff}, ui.Dp(150), cs)
blue := stack.End(dimensions)
stack.Layout(red, green, blue)
}
list := &layout.List{
Axis: layout.Vertical,
}
func drawList(c ui.Config, q input.Queue, list *layout.List, face text.Face, ops *ui.Ops, cs layout.Constraints) {
const n = 1e6
for list.Init(c, q, ops, cs, n); list.More(); list.Next() {
txt := fmt.Sprintf(“List element #%d", list.Index())
lbl := text.Label{Face: face, Text: txt}
dims := lbl.Layout(ops, list.Constraints())
list.Elem(dims)
}
list.Layout()
}
// Queue maps an event handler key to the events
// available to the handler.
type Queue interface {
Events(k Key) []Event
}
// Key is the stable identifier for an event handler.
// For a handler h, the key is typically &h.
type Key interface{}
func (b *Button) Layout(queue input.Queue, ops *ui.Ops) {
for _, e := range queue.Events(b) {
if e, ok := e.(pointer.Event); ok {
switch e.Type {
case pointer.Press:
b.pressed = true
case pointer.Release:
b.pressed = false
}
}
}
col := color.RGBA{A: 0xff, R: 0xff}
if b.pressed {
col = color.RGBA{A: 0xff, G: 0xff}
}
pointer.RectAreaOp{
Size: image.Point{X: 500, Y: 500},
}.Add(ops)
pointer.HandlerOp{Key: b}.Add(ops)
drawSquare(ops, col)
}
Takes all available events, updates it’s own state, system can know whether the events belong to this button or not is you register the area rectangle arc with a handler.
package app // import gioui.org/ui/app
func (w *Window) Queue() *Queue
import “gioui.org/ui”
import “gioui.org/ui/gesture”
import “gioui.org/ui/input”
var queue input.Queue
var c gesture.Click
for _, event := range c.Events(queue) {
// event is a gesture.ClickEvent, not a raw pointer.Event.
}
var cfg ui.Config
var s gesture.Scroll
distance := s.Scroll(cfg, queue, gesture.Vertical)
Complete implementation of a text area field. It’s a complicated widget, but is simple to use. You have to keep state somewhere, but you give it font and font size. Simply call the layout methods and
import “gioui.org/ui/text”
var faces measure.Faces
editor := &text.Editor{
Face: faces.For(regular, ui.Sp(52)),
}
editor.SetText(“Hello, Gophercon! Edit me.”)
editor.Layout(cfg, queue, ops, cs)
Gio is:
I want to bring Go from a place where GUI programming is a fringe activity to a state where it’s normal to use. Maybe in the future we can bring it to a place where you will choose Go for your GUI programming even if you aren’t interested in Go as a programming language, but because the tooling is so good.