window

package module
v0.8.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 10, 2026 License: BSD-3-Clause Imports: 13 Imported by: 0

README

go-widgets/window

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                │
└──────────────────────────────────────────────────────────────┘

Usage

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.

Backends

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
macOS Cocoa/AppKit backend (darwin)

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 puregono cgo. The OS-independent NSEventtoolkit.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).

Windows Win32/GDI backend (windows)

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).

wasmbox client backend (js/wasm)

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/*.rb on the pure-Go rbgo interpreter, baked into wasmbox.wasm). It boots the real desktop, spawns this client with the documented globalThis.wasmboxSpawnExternal("clients/gowidgets/worker.js") hook (a real external Worker + wasm instance over the step-C.1 MessagePort + 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 real page.mouse.click that the compositor routes to the focused window — asserting the counter goes 0→1 (input → toolkit.Event through 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 — see test/README-real-desktop.md.
  • Deterministic floor (test/probe-wasmbox.mjs) — the same assertions against test/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.

Public API

  • window.Open(cfg Config) (*Window, error) — dial $DISPLAY, authenticate, create and map the window. Linux only; returns window.ErrUnsupported elsewhere 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) into toolkit.Event and dispatch them, re-laying-out on resize.
  • (*Window).Close() error, (*Window).Size() (int, int).

Design notes

  • Sovereign protocol. internal/x11 speaks 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.PixelPainter renders into the backing RGBA buffer; the backend converts to the screen visual's pixel layout (channel masks + image byte order) and tiles PutImage under the server's maximum request length. A presentRect damage-region path is ready for when a scene damage list becomes available (toolkit exposes none today, so a full-surface present follows input).
  • Wayland is a separate future backend, intentionally out of scope here.

Verification

  • Unit tests run on amd64, arm64, and under qemu on riscv64, loong64, ppc64le, s390x — the big-endian wire path exercised on real big-endian (s390x) models, all strictly CGO=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 with import, asserts the sampled pixels, then synthesises a click and a key with xdotool and asserts the dispatched toolkit.Event.

License

BSD-3-Clause. Copyright (c) the go-widgets/window authors.

Documentation

Overview

Package window is a pure-Go (CGO-free, no Xlib/XCB) X11 windowing backend for the go-widgets toolkit. It opens a real window on an X11 server, blits the toolkit's RGBA framebuffer into it via the core protocol's PutImage, and routes X input events into toolkit.Event, so a go-widgets widget tree runs on a Linux desktop exactly as it does in the browser/wasm host.

The X11 protocol itself is implemented from scratch in the internal/x11 package over a raw byte stream, mirroring the sovereign transport+codec approach of github.com/go-freedesktop/dbus.

Open dials the server named by $DISPLAY; it is implemented on Linux and returns ErrUnsupported elsewhere, so cross-builds stay green. The windowing logic (framebuffer, present, event translation, run loop) is platform-independent and driven through the transport-agnostic internal/x11 connection.

Index

Constants

This section is empty.

Variables

View Source
var ErrUnsupported = errors.New("window: no native windowing backend for this platform")

ErrUnsupported is returned by Open on platforms with no windowing backend — everything that is neither Linux (X11/Wayland) nor macOS (Cocoa/AppKit) nor Windows (Win32/GDI) nor the js/wasm wasmbox environment.

Functions

This section is empty.

Types

type Backend

type Backend interface {
	// Run binds root, performs the initial layout+present, then dispatches
	// server/compositor events into the widget tree until the window closes.
	Run(root toolkit.Widget) error
	// Close releases the window and its connection.
	Close() error
	// Size returns the current client size in pixels.
	Size() (int, int)
	// String identifies the window for debugging.
	String() string
}

Backend is an open, backend-specific window bound to a go-widgets scene. The X11 (*Window), Wayland, macOS Cocoa, Windows Win32 and wasmbox backends all satisfy it, so Open can return whichever the environment selects and a go-widgets application is backend-agnostic: it just calls Run, Size, String and Close.

func Open

func Open(cfg Config) (Backend, error)

Open connects to the running display server and returns a window ready for Run. It auto-selects the backend: Wayland when $WAYLAND_DISPLAY is set (the modern default on contemporary Linux desktops), otherwise the X11 backend driven by $DISPLAY. Both are sovereign, pure-Go, CGO-free implementations of their wire protocols.

type Config

type Config struct {
	// Title is the WM_NAME shown in the title bar.
	Title string
	// Instance and Class populate WM_CLASS (window-manager grouping). When
	// empty they default to Title (Instance) and Title (Class).
	Instance string
	Class    string
	// Width and Height are the initial client size in LOGICAL points (the unit
	// the toolkit lays out and the user reads in — not device pixels). A value ≤ 0
	// asks the backend for a readable default: the macOS (Cocoa) backend derives
	// it from the main screen's visible frame; the X11/Wayland backends use their
	// standard 640×480. A desktop shell should pass its own point size here.
	Width  int
	Height int
	// Display overrides $DISPLAY (e.g. ":0"). Empty uses the environment.
	Display string
	// Theme overrides the toolkit theme used to paint the background and
	// widgets. Nil uses toolkit.DefaultDark.
	Theme *toolkit.Theme
}

Config parametrises a window.

type DamageRenderer added in v0.4.0

type DamageRenderer interface {
	// RenderDamaged paints this frame into p (the framebuffer painter, whose
	// clip seam confines each rectangle's repaint to the damage) and returns
	// the rectangles it repainted, in surface pixel coordinates. An empty
	// result means nothing changed this frame, so the backend presents nothing.
	// The returned slice need only stay valid until the backend has presented
	// it (which it does immediately, before the next frame).
	RenderDamaged(p painter.Painter, th *toolkit.Theme) []toolkit.Rect
}

DamageRenderer is the OPT-IN capability a root handed to Run may implement to drive incremental (damage-region) present instead of full-surface present.

A plain toolkit.Widget root keeps the full-surface path: every frame the whole framebuffer is repainted and blitted (correct, simple, unchanged). A root that ALSO implements DamageRenderer lets Run repaint and blit ONLY the rectangles that actually changed: Run draws the frame through RenderDamaged, takes the returned damage, and packs+presents just its (coalesced) union via the backend's small-rect present path (X11 MIT-SHM ShmPutImage over a framebuffer-mirroring segment, or a wl_shm sub-rect DamageBuffer). The very first frame, a resize and an X11 Expose still present the full surface — a resize because the framebuffer is reallocated, an Expose because the server discarded the window's contents — after which Run resumes incremental present.

github.com/go-widgets/toolkit/scene provides the reference implementation (scene.HostRoot), which is pixel-identical to a full repaint by construction; the interface is declared here, structurally, so the backend needs no import of the scene layer.

type Window

type Window struct {
	// contains filtered or unexported fields
}

Window is an open X11 window bound to a go-widgets scene. It owns the backing RGBA framebuffer, presents it to the server and drives the toolkit widget tree from X input events.

func (*Window) Close

func (w *Window) Close() error

Close closes the window's connection to the server.

func (*Window) Run

func (w *Window) Run(root toolkit.Widget) error

Run binds root to the window, performs the initial layout+draw+present, then dispatches server events into the toolkit until the window is closed (WM_DELETE_WINDOW) or the connection ends. It is the real-window analogue of the wasm compositor host loop.

func (*Window) Size

func (w *Window) Size() (int, int)

Size returns the current client size in pixels.

func (*Window) String

func (w *Window) String() string

String identifies the window for debugging.

Directories

Path Synopsis
cmd
gowidgetsclient command
windowdemo command
Command windowdemo opens a real native window showing a few go-widgets widgets, driven by the pure-Go github.com/go-widgets/window backend — an X11 or Wayland window on Linux, an NSWindow on macOS, a Win32 window on Windows.
Command windowdemo opens a real native window showing a few go-widgets widgets, driven by the pure-Go github.com/go-widgets/window backend — an X11 or Wayland window on Linux, an NSWindow on macOS, a Win32 window on Windows.
internal
cocoa
Package cocoa is the pure-Go (CGO-free, via purego) macOS AppKit windowing backend for the go-widgets toolkit.
Package cocoa is the pure-Go (CGO-free, via purego) macOS AppKit windowing backend for the go-widgets toolkit.
wasmbox
Package wasmbox implements the client half of the wasmdesk/wasmbox external-client wire protocol, so a go-widgets application can run as a client of the browser compositor exactly as it runs on X11 or Wayland.
Package wasmbox implements the client half of the wasmdesk/wasmbox external-client wire protocol, so a go-widgets application can run as a client of the browser compositor exactly as it runs on X11 or Wayland.
wayland
Package wayland is a from-scratch, pure-Go (CGO-free, zero non-stdlib dependency) implementation of the Wayland wire protocol, spoken directly over a UNIX-domain stream socket.
Package wayland is a from-scratch, pure-Go (CGO-free, zero non-stdlib dependency) implementation of the Wayland wire protocol, spoken directly over a UNIX-domain stream socket.
win32
Package win32 is the pure-Go (CGO-free) Windows Win32/GDI windowing backend for the go-widgets toolkit.
Package win32 is the pure-Go (CGO-free) Windows Win32/GDI windowing backend for the go-widgets toolkit.
x11
Package x11 is a from-scratch, pure-Go (CGO-free, zero non-stdlib dependency) implementation of the X Window System core protocol, version 11.0, spoken directly over a byte stream (a unix-domain socket in practice).
Package x11 is a from-scratch, pure-Go (CGO-free, zero non-stdlib dependency) implementation of the X Window System core protocol, version 11.0, spoken directly over a byte stream (a unix-domain socket in practice).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL