GopherCon 2019 - Simple, Portable and Efficient Graphical Interfaces in Go

By Christina Forney for the GopherCon 2019 Liveblog on July 25, 2019


Presenter: Elias Naur

Liveblogger: Christina Forney

Overview

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.


Why GUIs?

Last year at GopherCon we asked what the biggest challenges that Go developers faced. Here are the results:

Go challenges graph

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.

Introduction

Gio - gioui.org

  • Gio is a simple Go module for writing portable and fast graphical interfaces.

Scatter - scatter.im

  • Scatter is a Gio program for end-to-end encrypted messaging over email.

Demo - Scatter

Scatter is a multi-platform messaging application for sending and receiving encrypted chat messages, implementing the Signal protocol over federated email.

Scatter UI

Features

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.

    • UI state owned by program.
  • Only depends on lowest-level platform libraries.

    • Minimal dependency tree to keep things low level as possible.
  • GPU accelerated vector and text rendering.

    • It’s super efficient
  • No garbage generated in drawing or layout code.
  • Cross platform (macOS, Linux, Windows, Android, iOS, tvOS, Webassembly).
  • Core is 100% Go. OS-specific native interfaces are optional.

Immediate mode UI

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!

  • UI state is owned by the program. Even layout and widget tree.
  • No callbacks. Events are handled while drawing.

Blank window

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.

Hello, World

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()
}

Go challenges graph

Running Gio programs

Linux, macOS, Windows


Enable modules

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.

Build, install or run the program


go build gioui.org/ui/apps/hello
go install scatter.im/cmd/scatter
go run helloworld.go

Android

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

iOS/tvOS

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

Browsers

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.

Operations

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.

Operations


Serializing operations

import “gioui.org/ui” // Pure Go

var ops ui.Ops
// Add operations to ops
ui.InvalidateOp{}.Add(ops)

Only the app package depends on platform libraries

import “gioui.org/ui/app”

var w app.Window
w.Draw(&ops)

Position other operations

import “gioui.org/ui”

ui.TransformOp{ui.Offset(f32.Point{})}.Add(ops)

Request a redraw

ui.InvalidateOp{}.Add(ops) // Immediate
ui.InvalidateOp{At:}.Add(ops) // Delayed

Drawing operations


Set current color or image

import “gioui.org/ui/draw”

draw.ColorOp{Color: color.RGBA{}}.Add(ops)
draw.ImageOp{Src:, Rect:}.Add(ops)

Draw with the current color or image

draw.DrawOp{Rect: …}.Add(ops)

Clip operations


Clip drawing to a rectangle

import “gioui.org/draw”

draw.RectClip(image.Rectangle{}).Add(ops)

Or to an outline

var b draw.PathBuilder
b.Init(ops)
b.Line()
b.Quad() // Quadratic Beziér curve
b.Cube() // Cubic Beziér curve
b.End()

Input operations


Keyboard and text input

import “gioui.org/ui/key”

// Declare key handler.
key.HandlerOp{Key: handler, Focus: true/false}.Add(ops)

// Hide soft keyboard.
key.HideInputOp{}.Add(ops)

Mouse and touch input

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}

Drawing

Drawing (and animating)


Drawing and animating a clipped square

    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)

Layout

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.

Constraints and dimensions


Constraints are input

package layout // import gioui.org/ui/layout

type Constraints struct {
    Width  Constraint
    Height Constraint
}

type Constraint struct {
    Min, Max int
}

Dimensions are output

type Dimens struct {
    Size     image.Point
    Baseline int
}

Widgets accept constraints, output dimensions

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

Example - two labels

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

Layout helpers

Can layout to the compass directions or to specific place, like the center.

Aligning

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)

Insetting

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)

Flex layout


Lay out widgets on an axis.

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

Stack layout

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 := &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()
}

Input

Input queue and handler keys

// 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{}

Pointer event handling

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.

Window input queue


The Window’s Queue method returns an input.Queue for OS events.

package app // import gioui.org/ui/app

func (w *Window) Queue() *Queue

Gestures

import “gioui.org/ui”
import “gioui.org/ui/gesture”
import “gioui.org/ui/input”

Detect clicks

var queue input.Queue
var c gesture.Click
for _, event := range c.Events(queue) {
    // event is a gesture.ClickEvent, not a raw pointer.Event.
}

Determine scroll distance from mouse wheel or touch drag/fling

var cfg ui.Config
var s gesture.Scroll

distance := s.Scroll(cfg, queue, gesture.Vertical)

Widgets

Widgets - the Editor

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

Initialize the editor

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

Draw, layout and handle input in one call.

editor.Layout(cfg, queue, ops, cs)

Why Gio?

Gio is:

  • Simple. Immediate mode design, no hidden state.
  • Portable. The core of Gio is all Go.
  • Fast. GPU accelerated, very little per-frame garbage.
  • Convenient. Develop on desktop, deploy on mobile.
  • Public domain source (UNLICENCE). Dual licenced MIT to please your lawyers. Most importantly, Gio needs your help to succeed!

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.