Series: Building Go's GPU Ecosystem
- GoGPU: A Pure Go Graphics Library — Project announcement
- From Idea to 100K Lines in Two Weeks — The journey
- Pure Go 2D Graphics with GPU Acceleration — Introducing gg
- GPU Compute Shaders in Pure Go — Compute pipelines
- Go 1.26 Meets 2026 — Ecosystem overview
- Enterprise 2D Graphics Library — gg architecture
- Cross-Package GPU Integration — gpucontext
- Unified 2D/3D Graphics Integration — gg + gogpu
- Core Complete, Focus on GUI — Phase 2 announcement
- gogpu/ui v0.1.0 — First Release ← You are here
From "Focus on GUI" to First Release
In the last article, we announced that the core gogpu ecosystem was complete and all our energy was going into building a GUI toolkit. At that point gogpu/ui was at Phase 2 Beta with 55K lines of code, 6 widgets, and one design system.
Today, roughly three weeks later, we are tagging v0.1.0 — the first public release. The toolkit grew from 55K to 150K lines, from 6 to 22 interactive widgets, and from one to three design systems. The entire ecosystem went through a coordinated release — from shader compiler to widget toolkit — to bring everything onto stable, tagged versions.
This is a preview release. The API will change. Performance has not been optimized. There are rough edges. We are releasing now because we want community feedback to shape the API before it solidifies. The work is just beginning, but we believe there is enough here to start a conversation.
What Is gogpu/ui?
gogpu/ui is a GUI toolkit library for Go. You build widget trees, bind reactive state, and the toolkit handles layout, event dispatch, and rendering through gogpu/gg (a 2D graphics engine) onto a GPU surface provided by gogpu/gogpu (an application framework).
We studied 20+ GUI frameworks across multiple ecosystems — Flutter (Dart), Qt (C++), SwiftUI (Swift), Xilem/Floem/GPUI/Iced/Slint (Rust), Angular/SolidJS/React (Web), Fyne/Gio (Go) — and brought their proven patterns into Go:
- Pluggable Painter pattern (Flutter's separation of widget behavior from rendering)
- Reactive signals (SolidJS/Leptos-style fine-grained reactivity)
- Functional options for backward-compatible API evolution (Go-native)
- W3C Pointer Events for input handling (browser standard)
- CSS font-weight matching algorithm (W3C spec) for typography
-
Screen-space coordinate transforms (Flutter's
localToGlobal, Qt'smapToGlobal)
The entire stack — from shader compilation to window management to 2D rendering — is pure Go with zero CGO.
By the numbers (v0.1.0):
| Metric | Value |
|---|---|
| Interactive widgets | 22 |
| Design system themes | 3 (Material 3, Fluent, Cupertino) |
| Go source files | 350+ |
| Lines of code | ~146,000 |
| Test functions | ~3,900+ |
| Average test coverage | 97%+ |
| CGO required | No |
Quick Look: A Minimal Application
Here is a complete runnable application with a Material 3 button:
package main
import (
"fmt"
"log"
"github.com/gogpu/gg"
_ "github.com/gogpu/gg/gpu"
"github.com/gogpu/gg/integration/ggcanvas"
"github.com/gogpu/gogpu"
"github.com/gogpu/ui/app"
"github.com/gogpu/ui/core/button"
"github.com/gogpu/ui/primitives"
"github.com/gogpu/ui/render"
"github.com/gogpu/ui/theme/material3"
"github.com/gogpu/ui/widget"
)
func main() {
gogpuApp := gogpu.NewApp(gogpu.DefaultConfig().
WithTitle("My First App").
WithSize(600, 400).
WithContinuousRender(false)) // event-driven: 0% CPU when idle
m3 := material3.New(widget.Hex(0x6750A4)) // seed color
uiApp := app.New(
app.WithWindowProvider(gogpuApp),
app.WithPlatformProvider(gogpuApp),
app.WithEventSource(gogpuApp.EventSource()),
)
uiApp.SetRoot(
primitives.Box(
primitives.Text("Hello, gogpu/ui!").FontSize(24).Bold(),
button.New(
button.TextOpt("Click Me"),
button.OnClick(func() { fmt.Println("Clicked!") }),
button.PainterOpt(material3.ButtonPainter{Theme: m3}),
),
).Padding(24).Gap(12),
)
var canvas *ggcanvas.Canvas
gogpuApp.OnDraw(func(dc *gogpu.Context) {
w, h := dc.Width(), dc.Height()
if canvas == nil {
var err error
canvas, err = ggcanvas.New(gogpuApp.GPUContextProvider(), w, h)
if err != nil {
log.Fatal(err)
}
}
uiApp.Frame()
sv := dc.SurfaceView()
sw, sh := dc.SurfaceSize()
gg.SetAcceleratorSurfaceTarget(sv, sw, sh)
canvas.Draw(func(cc *gg.Context) {
cc.SetRGBA(0.94, 0.94, 0.94, 1)
cc.DrawRectangle(0, 0, float64(w), float64(h))
cc.Fill()
uiApp.Window().DrawTo(render.NewCanvas(cc, w, h))
})
_ = canvas.RenderDirect(sv, sw, sh)
})
gogpuApp.OnClose(func() { gg.CloseAccelerator() })
if err := gogpuApp.Run(); err != nil {
log.Fatal(err)
}
}
Yes, the boilerplate for wiring up the draw callback is verbose. That is an area we plan to improve. The tradeoff is full control over the render pipeline -- you can mix gg 2D drawing with UI widgets, render to textures, or integrate with your own rendering code.
The Widget Set
v0.1.0 ships 22 interactive widgets and display primitives:
Form controls: Button (4 variants, 3 sizes), Checkbox (tri-state), Radio group, TextField (with selection, clipboard, validation), Dropdown, Slider (continuous/discrete)
Containers: ScrollView (wheel, keyboard, scrollbar drag), TabView (lazy content, closeable tabs), Dialog (modal, focus trapping), Collapsible sections, SplitView (resizable panels)
Data display: ListView (virtualized, 1000+ items), GridView (virtualized 2D grid), DataTable (sortable columns, fixed header), TreeView (hierarchical, expand/collapse), LineChart (real-time, multiple series), ProgressBar, Circular Progress
Application chrome: Toolbar, Menu system (MenuBar + ContextMenu with submenus), Docking system (IDE-style panels)
Primitives: Box (VBox/HBox), Text (reactive via signals), Image, ThemeScope, RepaintBoundary (pixel caching)
Every interactive widget follows the same patterns: functional options for construction, a Painter interface for design-system independence, signal bindings for reactive state, and accessibility metadata (ARIA roles).
Three Design Systems
Widgets do not know how they look. Rendering is delegated to Painter implementations. v0.1.0 ships three:
Material Design 3 -- HCT color science, tonal palettes generated from a single seed color, 21 component painters.
Fluent Design -- Microsoft's design language with accent colors, inner focus rings, 9 painters.
Cupertino -- Apple HIG with iOS-style toggle switches, segmented controls, pill buttons, 9 painters.
Switching is one line:
// Material 3
btn := button.New(
button.TextOpt("Save"),
button.PainterOpt(material3.ButtonPainter{Theme: m3}),
)
// Fluent Design
btn := button.New(
button.TextOpt("Save"),
button.PainterOpt(fluent.ButtonPainter{Theme: fl}),
)
// Cupertino
btn := button.New(
button.TextOpt("Save"),
button.PainterOpt(cupertino.ButtonPainter{Theme: cu}),
)
The gallery example demonstrates live M3 seed color switching at runtime (Purple, Blue, Green, Orange). Fluent and Cupertino painters are implemented and tested but not yet wired into the gallery demo — switching between design systems at runtime is architecturally supported (just swap the Painter), but a full gallery with all three is a v0.2.0 goal.
Reactive Signals
State management uses a signals pattern built on our own coregx/signals library (inspired by SolidJS, Angular Signals, and Rust's Leptos). A Signal[T] holds a value. A Computed[T] derives from other signals and recomputes lazily. When a signal changes, bound widgets automatically invalidate and redraw.
count := state.NewSignal(0)
// Computed label updates automatically
label := primitives.TextFn(func() string {
return fmt.Sprintf("Clicked %d times", count.Get())
})
btn := button.New(
button.TextOpt("Increment"),
button.OnClick(func() { count.Set(count.Get() + 1) }),
)
Widgets support two-way signal bindings. The slider reads from and writes to the same signal:
volume := state.NewSignal[float32](50)
s := slider.New(
slider.Min(0), slider.Max(100),
slider.ValueSignal(volume), // two-way binding
)
// volume.Get() always reflects the current slider position
Signal lifecycle is automatic: widgets subscribe on mount and clean up on unmount. No manual disposal needed.
Architecture Highlights
Pluggable Painter pattern. Widget behavior (in core/) is separated from rendering (in theme/). A Button knows about click handling, focus states, and size variants. A ButtonPainter knows how to draw rounded corners and color fills. This avoids import cycles and lets third parties create entirely new design systems.
Functional options. All widget constructors use the options pattern for backward-compatible API evolution:
btn := button.New(
button.TextOpt("Submit"),
button.VariantOpt(button.Filled),
button.SizeOpt(button.Large),
button.OnClick(handleSubmit),
)
Adding new options in future versions will not break existing code.
Content[C] polymorphic pattern. Inspired by Taiga UI's Polymorpheus pattern (Angular), complex widgets like ListView, GridView, and TabView use a generic Content[C] interface from the CDK (Component Development Kit) package. This enables type-safe polymorphic content rendering — the widget provides the context (item index, selection state, hover), the user provides the builder function. Internally it powers cell recycling and virtualization, while externally users see a simple BuildItem(func(index int) widget.Widget) callback.
Retained-mode rendering. The widget tree tracks dirty state. Only changed widgets are redrawn. RepaintBoundary widgets cache their subtree as pixel buffers. Large boundaries (128x128+) automatically use tile-parallel rendering via scene.Scene with goroutine work-stealing.
Event-driven rendering. The default mode is 0% CPU when idle. The window only redraws when events arrive or signals change. No continuous render loop burning battery.
Accessibility from day one. Every widget implements the a11y.Accessible interface with ARIA roles, labels, and actions. The accessibility tree uses stable uint64 node IDs. Platform adapters (UIA, AT-SPI2, NSAccessibility) are not implemented yet -- that is one of the biggest remaining gaps.
What Works Well
After months of development, these areas feel solid:
- TextField handles Unicode input correctly (Latin, Cyrillic, CJK), with cursor movement, text selection, clipboard (Ctrl+C/V/X/A), and input validation.
- Tab focus navigation works across the widget tree with Tab/Shift+Tab. Focus rings render correctly.
- Hover cursors change appropriately (pointer for buttons, text beam for text fields, resize handles for SplitView).
- ScrollView dispatches events with correct coordinate transforms to child widgets, even when scrolled.
- Live theme color switching in the gallery demo (M3 seed color changes all widget colors instantly).
- Virtualized lists handle thousands of items with only visible rows rendered and recycled.
- Signal bindings propagate state changes through the widget tree without manual wiring.
What Needs Work (Honest Assessment)
This is a v0.1.0 preview. Here is what we know needs improvement:
- Performance. Complex widget trees (the full gallery) can feel sluggish. We have not done an optimization pass yet. Retained-mode rendering infrastructure is in place but not fully leveraged.
- Accessibility adapters. The a11y tree and ARIA roles are defined, but there are no platform adapters connecting to screen readers. This is a significant gap for production use.
- HiDPI. DPI scaling is wired through the stack but has not been thoroughly tested on all platforms.
- Documentation. API docs exist but guides and tutorials are sparse. The architecture document is 1200+ lines but aimed at contributors, not users.
- Draw callback boilerplate. The current setup code for wiring gogpu + gg + ui is too verbose. We plan to provide higher-level convenience functions.
- Some event edge cases. Drag cursors, scroll momentum, and overlay dismiss behavior have known rough spots.
- Platform testing. CI runs on Ubuntu, macOS, and Windows, but real-world testing has been primarily on Windows.
How to Try It
# Clone and run the gallery example
git clone https://github.com/gogpu/ui.git
cd ui/examples/gallery
go run .
Or add it to an existing project:
go get github.com/gogpu/ui@v0.1.0
go get github.com/gogpu/gogpu@latest
go get github.com/gogpu/gg@latest
The examples/ directory has four working applications:
| Example | What it shows |
|---|---|
examples/hello/ |
Checkboxes, radio buttons, ListView with 1000 items |
examples/signals/ |
Reactive state management patterns |
examples/taskmanager/ |
Real-time charts, progress bars, simulated system metrics |
examples/gallery/ |
All 22 widgets, M3 theme with live seed color switching |
Requirements: Go 1.25+, a GPU (Vulkan, Metal, DX12, or OpenGL ES -- software fallback available). No C compiler needed.
The gogpu Ecosystem
gogpu/ui sits at the top of a pure Go graphics stack:
naga -- shader compiler (WGSL to SPIR-V, MSL, GLSL)
wgpu -- WebGPU Hardware Abstraction Layer (Vulkan, Metal, DX12, GLES, software)
gogpu -- application framework (windowing, input, GPU context)
gg -- 2D graphics engine (Canvas API, GPU text rendering, SDF acceleration)
ui -- GUI toolkit (this project)
The entire stack is ~300K lines of pure Go. No CGO, no Rust FFI, no C bindings. If Go can build for a platform, the full stack runs there.
This release coincides with a coordinated cascade release across the entire ecosystem. All libraries were updated to work through wgpu's new public Core API (previously HAL-only):
| Library | Version | What Changed |
|---|---|---|
| naga | v0.14.7 | Shader compiler stability fixes |
| wgpu | v0.21.0 | New public API: Instance, Adapter, Device, Queue, Surface, CommandEncoder |
| gpucontext | v0.10.0 | Updated interfaces for new wgpu API |
| gogpu | v0.24.1 | Platform refactor, CharCallback for Unicode text input |
| gg | v0.37.0 | Migrated internal/gpu from HAL to wgpu public API, GPU RRect SDF clip |
| ui | v0.1.0 | This release |
This was a significant engineering effort — wgpu moved from internal HAL types to a proper public API with validation, state tracking, and resource lifecycle management. Every layer above had to adapt.
We Want Your Feedback
This release exists to start a conversation. We would genuinely appreciate input on:
- What widgets are missing for your use case?
- API ergonomics -- do functional options feel right for Go? Is the signal system intuitive?
- Performance expectations -- what is acceptable for the apps you want to build?
- Design system coverage -- are M3/Fluent/Cupertino the right targets? Missing painters?
- Documentation -- what would help you get started?
The Big Architecture Question: Monolith or Modular?
There is one question we have been debating internally and would especially value community input on:
Should gogpu/ui stay as a single module, or should we extract shared primitives into a separate foundation?
Right now, go get github.com/gogpu/ui pulls the entire toolkit — 56 packages, all widgets, all three design systems, all dependencies. If you only need geometry.Rect or the layout engine or the signal system, you still get everything.
We are considering extracting foundational packages into a separate gogpu/uikit (or similar) module:
| What | Currently in ui/
|
Could live in uikit/
|
|---|---|---|
| Geometry types | ui/geometry |
uikit/geometry |
| Layout engines | ui/layout |
uikit/layout |
| Event types | ui/event |
uikit/event |
| Widget base | ui/widget |
uikit/widget |
| Signal bindings | ui/state |
uikit/state |
| Theme interfaces | ui/theme |
uikit/theme |
This would let the community:
- Build custom widget libraries on top of shared primitives without importing all of gogpu/ui
- Create their own design systems (themes + painters) by depending only on the interfaces
- Extend our themes with custom component tokens without forking
- Use our layout engine (Flex, Grid, Stack) independently in their own UI frameworks
The tradeoff: another module to version and maintain, another API boundary to keep stable.
What do you think? Is a modular foundation important for your use case, or is a single go get simpler? We are genuinely unsure — the Go ecosystem has examples of both approaches working well. Your input will directly shape the v0.2.0 architecture.
File issues on GitHub, start a thread in Discussions, or just leave a comment here.
Links
- GitHub: github.com/gogpu/ui
- Release: v0.1.0
- Getting Started: docs/GETTING_STARTED.md
- Architecture: docs/ARCHITECTURE.md
- gogpu ecosystem: github.com/gogpu
Thank you for reading. We look forward to hearing what you think.
Top comments (0)