A pure-Go, CGO-free windowing backend for the
go-widgets toolkit, with five interchangeable
backends behind one Open/Run API — X11, Wayland, macOS
Cocoa/AppKit, Windows Win32/GDI and wasmbox (the
wasmdesk/wasmbox browser compositor).
Open auto-selects per environment: a real X11/Wayland window on Linux, a real
NSWindow on macOS, a real Win32 window on Windows, and — when built for
js/wasm — a wasmbox external client.
One go-widgets application runs unchanged natively AND inside wasmdesk.
The macOS backend reaches AppKit through the fleet's shared purego Objective-C
bridge go-macos/objc — no cgo; the Windows
backend reaches Win32/GDI through the process' own user32/gdi32/kernel32 DLLs via
syscall.NewLazyDLL and a syscall.NewCallback WNDPROC — no cgo — so both link
with CGO_ENABLED=0.
It implements the X11 core protocol (v11.0) from scratch over the unix
socket — no Xlib, no XCB, no cgo — the same sovereign transport + wire-codec
approach used by go-freedesktop/dbus.
It opens a real window on Linux, blits the toolkit's RGBA framebuffer into it
via the core-protocol PutImage, and routes X input into toolkit.Event.
┌──────────────────────────────────────────────────────────────┐
│ go-widgets/toolkit widget tree (Button, Label, VBox, …) │
├──────────────────────────────────────────────────────────────┤
│ window.Window layout → painter.PixelPainter → RGBA buffer │
│ X events → toolkit.Event → root.OnEvent │
├──────────────────────────────────────────────────────────────┤
│ internal/x11 sovereign X11 core protocol (from scratch) │
│ · wire codec (both byte orders) · setup handshake │
│ · MIT-MAGIC-COOKIE-1 Xauthority · keycode→keysym mapping │
│ · request/reply/error/event demux │
│ · PutImage (RGBA→visual pixel packing, max-request tiling) │
│ · MIT-SHM 1.2 fast path (shm fd over SCM_RIGHTS, ShmPutImage)│
├──────────────────────────────────────────────────────────────┤
│ unix socket /tmp/.X11-unix/X<n> → X server │
└──────────────────────────────────────────────────────────────┘
package main
import (
"github.com/go-widgets/toolkit"
"github.com/go-widgets/window"
)
func main() {
w, err := window.Open(window.Config{Title: "Demo", Width: 480, Height: 320})
if err != nil {
panic(err)
}
defer w.Close()
box := toolkit.NewVBox()
box.Append(toolkit.NewLabel("Hello from a pure-Go X11 window"))
box.Append(toolkit.NewButton("Click me", func() { /* ... */ }))
w.Run(box) // drives layout/draw/present + dispatches input until closed
}Run the bundled example: go run ./cmd/windowdemo.
Open returns a Backend (Run/Close/Size/String); the application is
backend-agnostic. The environment selects the implementation:
| GOOS/env | Backend | Transport |
|---|---|---|
Linux, $WAYLAND_DISPLAY set |
Wayland (internal/wayland) |
xdg-shell over the compositor unix socket |
Linux, else $DISPLAY |
X11 (internal/x11) |
X11 core protocol over the unix socket (+ MIT-SHM) |
macOS (darwin) |
Cocoa/AppKit (internal/cocoa) |
NSWindow + NSView via go-macos/objc (purego), NSBitmapImageRep present |
Windows (windows) |
Win32/GDI (internal/win32) |
top-level HWND via user32/gdi32 syscalls + NewCallback WNDPROC, StretchDIBits BGRA present |
js/wasm |
wasmbox (internal/wasmbox) |
wasmbox client protocol over a MessagePort + a SharedArrayBuffer surface |
| other (BSD, …) | stub → ErrUnsupported |
— |
On macOS Open creates a real NSWindow with a flipped content NSView,
presents the toolkit's RGBA framebuffer by wrapping it in an
NSBitmapImageRep drawn in -drawRect:, and decodes native
NSEvent mouse/scroll/key input into toolkit.Event. It honours the opt-in
DamageRenderer (only damaged rectangles are invalidated via
-setNeedsDisplayInRect: and re-blitted). Everything runs through
go-macos/objc over
purego — no cgo. The OS-independent
NSEvent→toolkit.Event mapping, flipped-view coordinate maths and
damage→dirty-rect conversion live in a sovereign, 100%-covered codec
(internal/cocoa/mapping.go); the darwin-only AppKit glue
(internal/cocoa/cocoa_darwin.go) is proven live on-device by the
darwin (cocoa) CI lane (open a window, render it, assert sampled pixels,
synthesise a click + key and assert the dispatched event + the button counter).
On Windows Open declares Per-Monitor-V2 DPI awareness, registers a window
class and creates a real titled, resizable top-level HWND, presents the
toolkit's RGBA framebuffer by packing it BGRA into a top-down 32bpp DIB and
blitting it with StretchDIBits on WM_PAINT, and decodes native WM_*
mouse/wheel/key messages into toolkit.Event. It honours the opt-in
DamageRenderer (only damaged rectangles are re-packed and InvalidateRect'd,
so WM_PAINT's update region blits just those). To stay readable on HiDPI it
renders the toolkit at logical size and lets the OS up-sample to the physical
client area (scale = GetDpiForWindow/96), rather than rendering at device
pixels and presenting into a smaller area. The whole path reaches Win32 through
the process' own user32/gdi32/kernel32 DLLs via syscall.NewLazyDLL and a
syscall.NewCallback WNDPROC — no cgo. The OS-independent WM_*→
toolkit.Event mapping, RGBA→BGRA DIB packing, DPI/size maths and
damage→InvalidateRect conversion live in a sovereign, 100%-covered codec
(internal/win32/mapping.go); the windows-only Win32 glue
(internal/win32/win32_windows.go) is proven live on-device on a Windows 11
arm64 QEMU VM — a real Win32 window rendering a VBox+Label+Button
(capture), with three injected
WM_LBUTTONDOWN/UP messages driving the button's counter 0 → 3 end to end
through the WNDPROC (after).
On js/wasm the environment is the wasmdesk/wasmbox
browser compositor, so instead of dialling a display server the backend runs as
an external client of the compositor: it allocates the surface
SharedArrayBuffer, posts hello over its per-client MessagePort, awaits
welcome, paints the widget tree into the SAB and posts commit — whole-surface,
or (when the root implements DamageRenderer, e.g. toolkit/scene.HostRoot)
just the damaged rectangles. Incoming input messages map to toolkit.Event
exactly as the X11/Wayland backends do. The wire protocol
(wasmbox docs/protocol.md)
is implemented in a sovereign, transport-agnostic codec (internal/wasmbox/protocol.go,
unit-tested to 100% on every GOOS); the syscall/js glue (client_js.go) only
carries the live JS handles. The wasmbox repository is not modified — this is
purely a client-side backend plus a worker shim.
Build the client and run it inside a compositor:
clients/gowidgets/build.sh # → clients/gowidgets/{gowidgets.wasm,wasm_exec.js}
# a wasmbox compositor spawns it via:
# wasmboxSpawnExternal("<origin>/clients/gowidgets/worker.js")The live browser proof (headless Chromium via Playwright, served by wasmbox's
own COOP/COEP cmd/serve) lives in test/, in two tiers:
- Real desktop (
test/probe-wasmbox-real.mjs) — drives the actual wasmdesk/wasmbox Ruby compositor (compositor/*.rbon the pure-Go rbgo interpreter, baked intowasmbox.wasm). It boots the real desktop, spawns this client with the documentedglobalThis.wasmboxSpawnExternal("clients/gowidgets/worker.js")hook (a real external Worker + wasm instance over the step-C.1MessagePort+ SAB), reads the compositor's own composited pixels (__wasmboxReadRegion) to assert the VBox+Label+Button rendered at the window's live focused rect, and injects a realpage.mouse.clickthat the compositor routes to the focused window — asserting the counter goes0→1(input →toolkit.Eventthrough the real input routing). Captured:test/wasmbox-live-proof-real-desktop-2026-08-09.png(the go-widgets window composited on the rbgo desktop, reading "Clicks: 1"). The wasmbox repo is unmodified; the client is served same-origin via a symlink overlay — seetest/README-real-desktop.md. - Deterministic floor (
test/probe-wasmbox.mjs) — the same assertions againsttest/harness.html, a protocol-faithful compositor stand-in, so the wire + SAB + input round-trip are exercised even without building the ~80 MB Ruby compositor. Captured:test/wasmbox-live-proof-2026-08-09.png.
window.Open(cfg Config) (*Window, error)— dial$DISPLAY, authenticate, create and map the window. Linux only; returnswindow.ErrUnsupportedelsewhere so cross-builds stay green.(*Window).Run(root toolkit.Widget) error— the host loop: initial layout/draw/present, then translate X events (Expose,KeyPress/Release,ButtonPress/Release,MotionNotify,ConfigureNotify,ClientMessage) intotoolkit.Eventand dispatch them, re-laying-out on resize.(*Window).Close() error,(*Window).Size() (int, int).
- Sovereign protocol.
internal/x11speaks the wire format byte-for-byte and is transport-agnostic (io.ReadWriteCloser), so the full request/reply/event machine is tested in-process against a scripted fake server — 100 % statement coverage on the codec, Xauthority parser and keysym mapping, both byte orders, error branches included. - Present. The toolkit's
painter.PixelPainterrenders into the backing RGBA buffer; the backend converts to the screen visual's pixel layout (channel masks + image byte order) and tilesPutImageunder the server's maximum request length. ApresentRectdamage-region path is ready for when a scene damage list becomes available (toolkitexposes none today, so a full-surface present follows input). - Wayland is a separate future backend, intentionally out of scope here.
- Unit tests run on
amd64,arm64, and under qemu onriscv64,loong64,ppc64le,s390x— the big-endian wire path exercised on real big-endian (s390x) models, all strictlyCGO=0. - A live X11 proof (
-tags=integration,WINDOW_X11_INTEGRATION=1) runs under Xvfb: it opens a window, presents a known four-quadrant pattern, captures it withimport, asserts the sampled pixels, then synthesises a click and a key withxdotooland asserts the dispatchedtoolkit.Event.
BSD-3-Clause. Copyright (c) the go-widgets/window authors.