Skip to content

Architecture

mike-ward edited this page Jun 14, 2026 · 2 revisions

Architecture

Go-gui is a hybrid immediate-mode GUI framework. Understanding what that means — and how it differs from retained-mode toolkits — explains most of the design decisions in the API.


Immediate-mode, explained

In a retained-mode framework, the UI is a tree of persistent objects. A button is an object with properties; you update those properties and the framework detects changes and re-renders the affected parts. The framework "retains" the widget tree and owns its state.

In an immediate-mode framework, there are no persistent widget objects. Every frame, a function runs and describes the entire UI from scratch. The framework renders what the function returns; nothing persists between calls.

Go-gui takes the immediate-mode approach for the user-facing API:

func myView(w *gui.Window) gui.View {
    app := gui.State[App](w)      // read current state
    return gui.Button(gui.ButtonCfg{
        Content: []gui.View{
            gui.Text(gui.TextCfg{Text: fmt.Sprintf("%d clicks", app.Clicks)}),
        },
        OnClick: func(_ *gui.Layout, e *gui.Event, w *gui.Window) {
            gui.State[App](w).Clicks++
            e.IsHandled = true
        },
    })
}

myView is called every frame. It reads state, constructs a layout tree, and returns it. There are no widget objects, no change detection, no lifecycle hooks.

The "hybrid" part: beneath the immediate-mode surface, a retained layout engine handles sizing, scroll state, focus tracking, and animations using stable IDs assigned by the programmer. This gives you the simplicity of immediate mode and the capability of a retained system. See Focus and Scrolling for how IDs work.


Frame pipeline

Each frame follows this sequence:

View function (user code)
  │
  ▼
GenerateViewLayout()
  │  Calls the view function, walks the returned View tree,
  │  and builds a Layout tree (Layout + Shape nodes).
  │
  ▼
layoutArrange()
  │  Walks the Layout tree, resolves sizing (Fit / Fixed / Grow),
  │  fills widths and heights, calculates spacing,
  │  and sets absolute X/Y coordinates on every Shape.
  │
  ▼
renderLayout() → []RenderCmd
  │  Walks the arranged tree and emits one RenderCmd per Shape:
  │  rect, text, circle, image, SVG, custom shader, clip, etc.
  │  Applies effects (color filters, blur, shadows).
  │
  ▼
Backend dispatch
     Metal (macOS) or OpenGL (Linux/Windows) executes the RenderCmd list.

There is no diffing, no virtual DOM, no incremental update. The full tree is rebuilt and re-rendered each frame. Performance comes from keeping the pipeline fast — value-type layout nodes, tessellation caches for SVG, GPU-accelerated rendering — not from avoiding work.


Core types

Window

The top-level container. Holds:

  • State slot — a single typed pointer to your application state (gui.State[T](w))
  • StateMap — per-widget internal state keyed by framework namespaces and user IDs
  • Layout tree — the arranged tree from the current frame
  • Animation registry — active animations, updated each frame
  • Command registry — keyboard shortcuts
w := gui.NewWindow(gui.WindowCfg{
    State:  &App{},
    OnInit: func(w *gui.Window) { w.UpdateView(myView) },
})

Layout

A node in the layout tree. Wraps a Shape and carries parent/child references:

type Layout struct {
    Shape    *Shape    // renderable properties
    Parent   *Layout
    Children []Layout
}

The tree is rebuilt every frame from View values. Layout nodes are value types — no heap allocation per node, no GC pressure from the view tree itself.

Shape

The concrete renderable. Carries all positional, visual, and behavioral properties:

  • Position and sizeX, Y, Width, Height, MinWidth, MaxWidth, etc. (absolute window coordinates after arrangement)
  • AppearanceColor, ColorBorder, SizeBorder, Radius, Opacity
  • LayoutAxis (row or column), Spacing, HAlign, VAlign, Sizing
  • IDsIDFocus (keyboard focus / tab order), IDScroll (scrollable container)
  • TextTC *ShapeTextConfig for text and rich content
  • EventsEvents *EventHandlers for callback wiring
  • EffectsFX *ShapeEffects for shadows, blur, filters

View

The interface that widget factories return:

type View interface {
    Content() []View
    GenerateLayout(w *Window) Layout
}

Widget factories like gui.Button(...) and gui.Column(...) return *Layout, which satisfies View. You compose views by nesting them in Content slices.

RenderCmd

A single draw operation emitted by renderLayout. The backend executes these as GPU draw calls. Users don't interact with RenderCmd directly except when writing custom backends.


State lives in the window, not widgets

This is a design principle worth internalizing. Because view functions are called every frame and widget objects don't persist, state cannot live in widgets. It lives in two places:

Per-window user state — your application data:

app := gui.State[App](w)   // retrieve
app.Count++                 // mutate in place — no notification needed

Per-window framework state (StateMap) — internal widget state (scroll offsets, input buffers, checkbox toggles). Keyed by IDs assigned in the view function. You don't manage this directly; widgets manage their own entries transparently.

This separation keeps the mental model clean: your application data is yours, the framework's bookkeeping is the framework's.


Package map

github.com/go-gui-org/go-gui/
│
├── gui/                          Core framework
│   ├── view*.go                  View interface, GenerateViewLayout — one file per widget (50+)
│   ├── layout*.go                Layout tree, arrangement, sizing
│   ├── shape*.go                 Shape type, ShapeTextConfig
│   ├── render*.go                renderLayout, RenderCmd, gradients, filters
│   ├── window*.go                Window, lifecycle, state
│   ├── event*.go                 Event types, dispatch, handlers
│   ├── animation*.go             Animation subsystem (easing, keyframe, spring, tween, hero)
│   ├── command*.go               Keyboard shortcuts, command registry
│   ├── a11y*.go                  Accessibility tree
│   ├── theme*.go                 Theme, theme registry, seed-color generation
│   ├── styles*.go                Per-widget style defaults
│   ├── locale*.go                Locale detection, number/date/currency formatting
│   ├── inspector*.go             Live layout tree debugger
│   ├── time_travel*.go           Frame-level state replay
│   ├── gesture.go                Touch gesture recognition
│   ├── input_mask.go             Text input masking
│   ├── spell_check.go            OS-level spell check integration
│   ├── shader.go                 Custom fragment shader support (Metal + GLSL)
│   ├── markdown*.go              Markdown rendering (math, Mermaid, highlight)
│   ├── print*.go                 PDF generation, print pipeline
│   ├── system_tray.go            System tray icon and menu
│   ├── titlebar.go               Dark/light titlebar control
│   ├── opt.go                    Opt[T] — optional numeric config values
│   └── ...
│
├── gui/svg/                      SVG parse, tessellation, animation, hit test
├── gui/audio/                    Audio playback (sound effects, music)
├── gui/datagrid/                 DataGrid data source contracts, CSV/XLSX/PDF export
├── gui/highlight/                Syntax highlighting (Chroma integration)
├── gui/locales/                  Built-in locale presets (de, en, es, fr, ja, etc.)
├── gui/markdown/                 Markdown parser, fetcher, style
├── gui/shader/                   Custom shader types and cache
├── gui/assets/                   Embedded assets and default icon
│
├── gui/backend/
│   ├── sdl2/                     SDL2: windowing, input, text measurement, SVG, audio
│   ├── metal/                    Metal renderer (macOS)
│   ├── gl/                       OpenGL renderer (Linux/Windows)
│   ├── web/                      WASM/Canvas2D renderer
│   ├── ios/                      Metal + UIKit (iOS)
│   ├── android/                  Android backend
│   ├── test/                     Headless no-op backend for unit tests
│   ├── filedialog/               Native file dialogs
│   ├── printdialog/              Native print dialogs
│   ├── nativemenu/               OS-native menu bar
│   ├── atspi/                    AT-SPI accessibility (Linux)
│   ├── sni/                      System tray (StatusNotifierItem)
│   └── spellcheck/               Spell checking
│
└── examples/                     50+ runnable applications
    ├── get_started/              Minimal stateful app
    ├── showcase/                 Interactive widget reference
    ├── todo/                     Classic todo app
    ├── calculator/               Styled calculator
    └── ...

Backend interface

The core package does not import any backend. Instead, backend interfaces are injected at startup by the SDL2 backend:

  • TextMeasurer — font metrics, line wrapping
  • SvgParser — SVG parse, tessellation, hit testing
  • NativePlatform — composes dialogs, notifications, accessibility, IME, system tray, spell checking, native menubar, printing, bookmarks, and URI opening

This separation means the gui/ package compiles without any C dependencies and can run headlessly in tests.


The Opt[T] pattern

Widget config structs use Opt[T] for all numeric fields:

type ButtonCfg struct {
    Padding   Opt[float32]   // zero = use theme default
    FontSize  Opt[float32]   // gui.SomeF(18) = explicit override
    // ...
}

Opt[T] is a simple optional: zero value means "use the theme default"; gui.SomeF(v) sets an explicit override. This lets widgets look correct out of the box and lets you override selectively without re-specifying every field.


Further reading

Clone this wiki locally