-
Notifications
You must be signed in to change notification settings - Fork 1
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.
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.
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.
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) },
})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.
The concrete renderable. Carries all positional, visual, and behavioral properties:
-
Position and size —
X,Y,Width,Height,MinWidth,MaxWidth, etc. (absolute window coordinates after arrangement) -
Appearance —
Color,ColorBorder,SizeBorder,Radius,Opacity -
Layout —
Axis(row or column),Spacing,HAlign,VAlign,Sizing -
IDs —
IDFocus(keyboard focus / tab order),IDScroll(scrollable container) -
Text —
TC *ShapeTextConfigfor text and rich content -
Events —
Events *EventHandlersfor callback wiring -
Effects —
FX *ShapeEffectsfor shadows, blur, filters
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.
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.
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 neededPer-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.
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
└── ...
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.
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.
- Getting Started — first app walkthrough
- Focus and Scrolling — the ID system in depth
- Events — event routing and dispatch rules
- Layouts — sizing modes, flex model, container types
-
docs/architecture.mdin the source repository — additional pipeline diagrams
Getting Started
Widgets
Layout & Interaction
Visuals
Reference