Architecture

Gophics is one idea carried all the way down: a pipeline. You describe the UI as immutable values; the framework diffs them into a retained tree, lays it out once, and composites it on the GPU — every frame, in pure Go.

Two trees, and why

Everything below rests on one split, so it is worth getting first. A widget is an immutable description — a plain struct value, cheap to make, thrown away every frame. An element is the retained node the framework keeps behind it: it holds your state, and it persists across rebuilds.

// The widget: a value. Rebuilt constantly, owns nothing.
type Counter struct{ Start int }

// The state: kept alive by the element behind that widget.
type counterState struct {
        widget.StateBase[Counter]
        n int
}

func (s *counterState) Build(ctx widget.Ctx) widget.Widget {
        return theme.Button{
                Label:  fmt.Sprintf("%d", s.n),
                OnTap:  func() { s.SetState(func() { s.n++ }) },
        }
}

So Build is not a draw call and describes nothing about pixels. It returns a fresh description; the framework diffs it against the element tree and touches only what actually differs. That is what makes rebuilding the whole subtree on every tap cheap, and it is why widgets can be compared, serialized, and asserted on in a test without a window.

The frame pipeline

Those two trees are what the frame runs over. Each frame flows through the same phases (mirroring the model that made own-rendering UIs fast), and every phase is independently instrumentable and testable:

      input       gestures and callbacks mark widgets dirty
      tickers     animations advance, marking more
        ↓
      build       rebuild dirty widgets, reconcile against the element tree
        ↓
      layout      constraints down, sizes up — one pass, with relayout boundaries
        ↓
      paint       dirty nodes record display lists into a layer tree
        ↓
      composite   layer tree → GPU commands → present

Input and tickers are the two things that start a frame; the four phases below them are the frame itself.

Only what changed is redone, and that is the point of the whole arrangement rather than an optimisation on top of it. Each phase has its own boundary: a rebuild stops where the description is unchanged, a relayout stops at a node whose constraints did not move, and a repaint stops at the layer whose damage rectangle covers it. Typing a character in a text field does not re-lay-out the page around it, and a one-widget change costs about one widget's worth of work.

The layers

Each concern is a small Go package; dependencies point downward. You build apps against the top; everything below is the engine.

widget   Widget/Element trees, State (StateBase[W]), keys, focus, reconciler, gestures (Handler)
theme    design tokens & styled components (opt-in)          anim  tickers, curves, controllers
──────────────────────────────────────────────────────────────────────────────────────────
layout   the box protocol — BoxConstraints down, sizes up; flex, stack, padding, viewports
──────────────────────────────────────────────────────────────────────────────────────────
scene    retained display lists, diffed for damage      paint  Canvas: paths, gradients, layers, text
text     shaping · bidi · line-breaking (go-text)        geom   points, rects, rrects, affine transforms
──────────────────────────────────────────────────────────────────────────────────────────
app      the runtime: app.Run (a window) · app.Headless (tests)   shell  per-platform window / input / present
internal/gfx   the vendored, pure-Go WebGPU + shader-compiler + 2D-renderer substrate (zero CGo)

Own-rendering, in pure Go

Gophics draws every pixel itself rather than wrapping native widgets or a webview — that's why one widget tree looks the same on every platform. The rendering substrate is vendored in-tree and is entirely pure Go (no CGo): a WebGPU implementation, a WGSL shader compiler, and an analytic-AA 2D renderer. The shell layer presents that output per platform:

Testable without a display

Because the renderer has an offscreen path, the whole UI is a rendering library: app.Headless mounts a widget tree, drives taps/keys/frames, and returns an image — no window, no emulator. Layout, painting, gestures, and full widget trees all run under go test, and golden-image tests are a first-class feature. A server can render a widget tree to a PNG or a report with no display at all.

Get started →  ·  The code on GitHub ↗