Help us shape the future of Go! Take the 2026 Go Developer Survey

window

package module
v0.89.2 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: BSD-3-Clause Imports: 24 Imported by: 7

README

go-widgets/window

A pure-Go, CGO-free windowing backend for the go-widgets toolkit, with eight interchangeable backends behind one Open/Run API — X11, Wayland, GTK4 (opt-in), macOS Cocoa/AppKit, Windows Win32/GDI, Android, wasmbox (the wasmdesk/wasmbox browser compositor) and a browser tab <canvas>. Open auto-selects per environment: a real X11/Wayland window on Linux, a real NSWindow on macOS, a real Win32 window on Windows, a real Activity surface on Android, and — when built for js/wasm — a wasmbox external client, or a <canvas> in an ordinary browser tab. 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 godbus/dbus/v5. 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, $GO_WIDGETS_GTK set GTK4 (internal/gtk), opt-in GTK owns the window; the framebuffer is a GtkPicture, and native controls (toolkit.NativeControl) are real GTK widgets above it. Needs the libgtk-4 runtime
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
Android, $GW_ANDROID_SOCKET set Android host (go-widgets/android) framed protocol over an abstract LocalSocket + a memfd surface shared with the Java host
Android, else Wayland or X11, as on Linux a shell under Termux still has a display server to dial
js/wasm wasmbox (internal/wasmbox) wasmbox client protocol over a MessagePort + a SharedArrayBuffer surface
js/wasm, an ordinary page browser tab (internal/tab) a <canvas> through go-widgets/webcanvas: no compositor, no SharedArrayBuffer, no cross-origin isolation
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 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).

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

Android backend (android)

Android is the one target where a CGO-free process cannot own a window at all: the entire graphics and input API sits behind JNI, so there is no syscall-level surface to claim the way X11, Wayland, Win32 and AppKit each offer one. The backend answers that by not trying — the application runs as the Go half of go-widgets/android, where a thin Java host owns the Activity and the SurfaceView, and the Go side owns layout, widgets, theme and hit-testing. Pixels cross through a memfd both processes map (Go writes RGBA_8888, which is Android's ARGB_8888 byte for byte, so the blit is a plain copy); input, insets, IME text and the accessibility tree cross a framed protocol over an abstract LocalSocket.

GOOS=android names two environments, and Open distinguishes them by the one fact only a host can produce: it exports $GW_ANDROID_SOCKET when it spawns the application. Set means an APK, and the host is dialled. Unset means a shell — under Termux, against Termux:X11 or a Wayland compositor — where the ordinary Linux path is both right and available, so Open falls through to it. One binary serves both, chosen by what is actually there rather than by a build tag.

⚠ android/arm64 is the only Android target Go links CGO-free; arm, amd64 and 386 all require external cgo linking. CI asserts both halves of that — the one that works and the three that do not — so the day Go widens it, the build says so.

Browser tab backend (js/wasm, an ordinary page)

When the page is not a wasmbox client -- it has a document and the wasmbox worker never installed its hook -- Open returns a backend that draws into a <canvas> of the page (Config.Canvas, default "screen") through go-widgets/webcanvas. The page needs nothing special: no COOP/COEP headers, so it can be served as static files from anywhere, behind any web tier. The same w.Run(root) drives the tree: pointer, wheel and keyboard become toolkit events carrying Ctrl, Shift, Alt and Meta, drag-and-drop goes through the same controller as the other backends, the canvas follows the page's layout, and a root that renders incrementally repaints only its damage. It is a Repainter like the native backends: Repaint, from any goroutine, draws the next animation frame, so a result that arrives with nobody touching the page still reaches it.

<canvas id="screen" style="width:100vw;height:100vh"></canvas>
<script src="wasm_exec.js"></script>
<script>
  const go = new Go();
  WebAssembly.instantiateStreaming(fetch("app.wasm"), go.importObject).then(r => go.run(r.instance));
</script>
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) (Backend, error) — open a window on the back-end the environment calls for (see Backends). Where there is none (the BSDs, …) it returns window.ErrUnsupported, so cross-builds stay green.

  • Backend.Run(root toolkit.Widget) error — the host loop: initial layout/draw/present, then translate the platform's input (on X11: Expose, KeyPress/Release, ButtonPress/Release, MotionNotify, ConfigureNotify, ClientMessage) into toolkit.Event and dispatch it, re-laying-out on resize.

  • Backend.Close() error, Backend.Size() (int, int), Backend.String() string.

  • window.Screens() ([]Screen, error) — enumerate the attached displays, primary first, in logical points with the desktop's panels excluded. Safe to call before Open, since picking an output is something an application does on the way in. macOS answers through Cocoa, X11 through RANDR 1.5 (shared with the screen capture in go-freedesktop/x11, not a second copy of it), Wayland through wl_output, and Windows through EnumDisplayMonitors (shared with go-mswin/screencapture through go-mswin/win32). Only js/wasm still returns window.ErrScreensUnsupported.

    Screen.Name is the panel's own name where the platform offers one — the product string out of its EDID, "DELL U2720Q" — falling back to the connector ("HDMI-1", "DP-2", \\.\DISPLAY1) on a display that publishes none, and also when two attached displays publish the same one: two identical monitors say the identical thing about themselves, and a name that cannot tell them apart is not a name. That is what an application recognising a particular headset has to match on.

    On Windows the EDID lives in the registry under the monitor's device instance, and a panel with none falls back to the description its driver gives — which for the inbox monitor driver is "Generic PnP Monitor" for every panel attached, hence the rule above. Windows is also the one platform with no single logical coordinate space: each display's rectangle is in its own points, so on a mixed-DPI desktop they do not tile. Nothing depends on them tiling — Config.Screen takes the Screen value back and the back-end re-resolves it — but a caller doing arithmetic across two Windows displays should know.

  • window.VisibleScreenSize() (w, h int, ok bool) — the usable area of the primary display, superseded by Screens for anything multi-display.

  • window.Show(b), window.Hide(b), window.Raise(b) — the Visibility capability: take an open window off the screen without closing it (Run keeps running and the widget tree keeps its state), put it back, and bring it to the front with the keyboard focus. It is what a system-tray application needs: its "Open" item raises the window already there, and its window can hide while the application carries on in the tray. Safe from any goroutine; each returns once the request is on its way to the window's own thread. A back-end that cannot do one says so with window.ErrNotSupported, and a closed window answers window.ErrClosed.

    back-end Show Hide Raise
    X11 MapWindow ICCCM withdraw (unmap + synthetic UnmapNotify), so it leaves the taskbar map, stack above, EWMH _NET_ACTIVE_WINDOW
    Wayland the initial commit again, then the next configure maps it null buffer + commit (unmap) ErrNotSupported: xdg-shell has no such request, and xdg-activation needs a token from the user's input to this window
    macOS (Cocoa) orderFront: orderOut: out of the Dock, activate the app, makeKeyAndOrderFront: (a passive window is only ordered front)
    Windows (Win32) SW_SHOWNA SW_HIDE SW_RESTORE/SW_SHOW + SetForegroundWindow
    GTK, Android, wasmbox ErrNotSupported ErrNotSupported ErrNotSupported

    Focus is the platform's to grant: under focus-stealing prevention a window manager, or Windows' foreground lock, may mark the window as wanting attention instead, and Raise cannot tell. Under an X11 window manager a window shown again is placed by the manager, as a new one would be.

Results from other goroutines

Every backend that has a run loop to wake is a Repainter: X11, Wayland, GTK, macOS, Windows and the browser tab. Repaint is safe from any goroutine, but widget state is not, so post the change to an mvvm.Queue whose wake is Repaint (Queue is in go-widgets/mvvm v0.14.0 or later; this module itself builds against an older mvvm, so an application that uses it requires github.com/go-widgets/mvvm at v0.14.0 or above in its own go.mod), and

drain it in the root container's SetBounds, which every backend calls before it draws:

w, _ := window.Open(cfg)
var wake func()
if r, ok := w.(window.Repainter); ok {
	wake = r.Repaint
}
q := mvvm.NewQueue(wake)
go func() { v := fetch(); q.Post(func() { vm.Result.Set(v) }) }()

type root struct{ *toolkit.VBox; q *mvvm.Queue }
func (r *root) SetBounds(b toolkit.Rect) { r.q.Drain(); r.VBox.SetBounds(b) }

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 root that implements DamageRenderer (toolkit/scene.HostRoot is the reference) reports the rectangles it repainted, and X11 (MIT-SHM ShmPutImage), Wayland (wl_shm damage), Cocoa, Win32 and wasmbox present only those. The first frame, a resize and an X11 Expose still present the whole surface.

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. TestLiveX11Visibility hides, shows and raises real windows there and reads the result from the server (xdotool search --onlyvisible, and the pixel on top where two windows overlap); the same assertions are made against scripted fakes in-process. The Cocoa lane's TestLiveShowHideRaise… asks a real NSWindow from another goroutine and reads -isVisible / -isMiniaturized back from AppKit. The Wayland request order is proven against a fake compositor that enforces xdg-shell's map/unmap rules; the Win32 decision table is unit-tested, and its UI-thread calls are compile-verified only.

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 a pure-Go wire library such as github.com/godbus/dbus/v5.

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

View Source
const NativeScale = -1.0

NativeScale asks for a framebuffer at the display's own resolution rather than one pixel per logical point. See Config.RenderScale for when that is the right thing to ask for -- it is a narrower case than it sounds.

Variables

View Source
var ErrClosed = errors.New("window: the window is closed")

ErrClosed is returned by a request made of a window that has been closed.

View Source
var ErrNoScreens = fmt.Errorf("window: no screens in the display list")

ErrNoScreens is what a back-end that could not read the display list reports.

It is deliberately NOT ErrScreensUnsupported: that one means this platform has no way to enumerate at all, which is a fact about the build. This one means the enumeration ran and came back with nothing, which is a fact about the moment and may be different a second later.

View Source
var ErrNotSupported = errors.New("window: not supported by this back-end")

ErrNotSupported is what a back-end answers for something its platform will not let it do. It is wrapped with the reason, so errors.Is finds it and the message still says why.

View Source
var ErrScreensUnsupported = errors.New("window: this back-end cannot enumerate screens yet")

ErrScreensUnsupported is returned by Screens on a back-end that cannot yet enumerate displays. It is not a failure to handle defensively so much as a statement of coverage: today macOS answers through Cocoa, X11 through RANDR, Wayland through wl_output and Windows through EnumDisplayMonitors, and js/wasm does not.

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

func Hide added in v0.86.0

func Hide(b Backend) error

Hide takes b's window off the screen without closing it; see Visibility. A back-end without the capability answers ErrNotSupported.

func Raise added in v0.86.0

func Raise(b Backend) error

Raise brings b's window to the front and asks for the focus; see Visibility. A back-end without the capability answers ErrNotSupported.

func Show added in v0.86.0

func Show(b Backend) error

Show makes b's window visible again; see Visibility. A back-end without the capability answers ErrNotSupported.

func SystemFontTTF added in v0.53.0

func SystemFontTTF() ([]byte, error)

SystemFontTTF returns the raw sfnt bytes of the host's UI face, ready for toolkit.NewTrueTypeFont, and an error when the platform has no such file to offer.

It is the same answer as the AppearanceReader method of that name, asked WITHOUT a window — which is the whole reason it exists. An application that wants to be drawn in the system face has to install that font before it lays anything out, because the font decides how tall a line of text is and therefore how tall the window has to be. Reaching the method means opening the window first, so the size would have to be computed from the font the window is not going to use. Every one of these back-ends reads a file, which needs no window and no display server, so the question is answerable on the way in.

It is NOT cheap -- the macOS system face is tens of megabytes -- so it is asked once at startup, unlike Appearance, which is cheap enough to poll.

macOS answers from /System/Library/Fonts, Windows from the Segoe UI file, and both Linux back-ends report an error: a Linux desktop names a font family and leaves finding it to fontconfig, which is a font library's job and not a window's.

func VisibleScreenSize added in v0.27.0

func VisibleScreenSize() (w, h int, ok bool)

VisibleScreenSize returns the usable area of the primary display in LOGICAL points — on X11 the full panel minus whatever the desktop reserved through _NET_WM_STRUT. ok is false when no display server can be reached, or when it reports no display.

See Screens, which supersedes it for anything multi-display: it reports every attached panel, not only the primary one.

Types

type Appearance added in v0.14.0

type Appearance struct {
	// Dark is the effective dark/light mode.
	Dark bool
	// Accent is the user's accent colour, meaningful only when HasAccent is
	// set. A system too old to have the notion, or one where the user made no
	// choice, reports HasAccent false rather than a made-up colour.
	Accent    color.RGBA
	HasAccent bool
}

Appearance is the host UI's look: what the user has told their system they want everything to look like.

A go-widgets app picks its own theme, which is right for an app with a designed identity and wrong for one that should feel native. An app that wants to belong on the desktop it is running on needs to know that the user chose dark mode and picked purple as their accent, and no amount of theming inside the toolkit can discover that: it is a platform fact.

type AppearanceReader added in v0.14.0

type AppearanceReader interface {
	Appearance() Appearance
	SystemFontTTF() ([]byte, error)
}

AppearanceReader is an optional Backend capability: reading the host look.

Appearance is cheap enough to poll -- a handful of platform queries, no allocation -- so a back-end need not push changes and an app can simply ask each frame and act when the answer differs. That keeps the seam a plain question instead of a callback with a lifetime.

SystemFontTTF is separate precisely because it is NOT cheap: the macOS system face is tens of megabytes on disk, so it is asked for once at startup, not on every poll. It returns the raw sfnt bytes, ready for toolkit.NewTrueTypeFont, and an error when the platform has no such file to offer.

Implemented by macOS (Cocoa), Windows (the registry's personalisation keys) and both Linux back-ends, which read the same XDG desktop portal — the desktop look is not a property of the display server carrying the pixels.

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 returns a live window backed by this environment's display server. It auto-selects: Wayland when $WAYLAND_DISPLAY is set, X11 otherwise.

Setting $GO_WIDGETS_GTK opts into the GTK4-hosted backend instead — GTK owns the window, the toolkit's framebuffer goes in a GtkPicture and native controls (toolkit.NativeControl) are real GTK widgets overlaid above it. It is opt-in so the default stays the dependency-free from-scratch backends; the GTK path needs the libgtk-4 runtime present.

Android is excluded here and answered by open_android.go, because a binary there may be running inside an APK, where there is no display server to dial at all.

type Clipboard added in v0.13.0

type Clipboard interface {
	// ClipboardText returns the pasteboard's plain-text contents, or "" when it
	// holds no text (an image, a file promise, or nothing at all).
	ClipboardText() string
	// SetClipboardText replaces the pasteboard's contents with text.
	SetClipboardText(text string)
}

Clipboard is an optional Backend capability: the host OS text clipboard.

Copy and paste inside a go-widgets app already work through the toolkit's own in-process clipboard. What that cannot do is carry text ACROSS applications — paste a URL from a browser into a text field, or copy an article's title out to somewhere else — because the OS pasteboard is a platform facility and the toolkit is deliberately platform-free.

A back-end that can reach the pasteboard implements this. Its method set is deliberately identical to toolkit.Clipboard, so an app installs it in one line and every text widget's copy/cut/paste starts going through the real OS pasteboard:

w, err := window.Open(cfg)
...
if c, ok := w.(window.Clipboard); ok {
	toolkit.SetClipboard(c)
}

That line is the app's to write, not Open's: reaching into a package-level toolkit setting is a decision an app makes, not a side effect a constructor should have. A back-end that cannot reach a pasteboard simply does not implement this, the assertion fails, and the toolkit's in-process clipboard stays in place — copy/paste still works within the app.

Implemented by every back-end that has a pasteboard to reach: macOS through NSPasteboard, Windows through the Win32 clipboard, X11 through selection ownership and Wayland through wl_data_device.

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
	// RenderScale is how many framebuffer pixels the back-end allocates per
	// logical point.
	//
	// Zero, the default, is one pixel per point. The UI is laid out and painted
	// at a readable size and the compositor up-samples it to a HiDPI display,
	// which is slightly soft but correct for every widget tree: the toolkit lays
	// out in the same units it paints in, so a framebuffer twice as wide would
	// give a window full of widgets at half the size.
	//
	// [NativeScale] follows the display's backing factor, giving a framebuffer at
	// the panel's true resolution. It is CORRECT ONLY FOR A ROOT THAT RENDERS ITS
	// OWN PIXELS at the size it is given -- a [toolkit.Surface] over an
	// application's own scene -- because such a root is told the render-pixel
	// size and composes for it, so nothing is laid out in the wrong unit. Passing
	// it with an ordinary widget tree is not an error the back-end can detect; it
	// simply makes everything half-size on a 2x display.
	//
	// Any other positive value is used as-is.
	//
	// Honoured today by the macOS (Cocoa) back-end.
	RenderScale float64
	// Screen places the window on a particular display, as returned by
	// [Screens]. Nil, the default, lets the platform choose -- which in practice
	// means the display the desktop considers active.
	//
	// The value is re-resolved against the displays attached when Open runs, so a
	// display unplugged in between is reported as an error rather than becoming a
	// window at coordinates that no longer describe anything. That case is
	// ordinary with an external panel, and routine with an XR headset.
	//
	// Honoured today by the macOS (Cocoa) back-end.
	Screen *Screen
	// Canvas is the id of the <canvas> a browser tab draws into, when the page
	// is an ordinary tab rather than a wasmdesk/wasmbox client. Empty means
	// "screen". Ignored by every other back-end.
	Canvas string
	// Fullscreen sizes the window to cover its screen entirely, with no title bar
	// and no frame. With Screen nil it covers the primary display.
	//
	// It is NOT the platform's native full-screen mode: on macOS there is no
	// Space, no animation and no menu bar at the top edge. A borderless window at
	// the panel's exact bounds is what an immersive surface needs -- every pixel
	// of the display, and nothing of the desktop -- and it is what allows one to
	// sit on an external output while the desktop carries on elsewhere.
	//
	// Honoured today by the macOS (Cocoa) back-end.
	Fullscreen bool
	// FixedSize makes the window unresizable: no resize control, no drag on an
	// edge, no zoom button.
	//
	// For a window sized to its own content there is nothing to gain from
	// resizing it and something to lose. Room has to be found for what a smaller
	// window cannot show, and the honest answers are a scrollbar in a dialogue
	// that never needs to scroll, or a reflow into a layout nobody designed. A
	// settings window is the case in point: it is exactly as big as what it has
	// to say.
	//
	// Resizable stays the default, because most windows hold content whose size
	// is the user's business rather than the layout's.
	//
	// Honoured today by the macOS (Cocoa) back-end; elsewhere it is accepted and
	// ignored, so a caller need not ask what platform it is on.
	FixedSize bool
	// Passive makes the window a picture and nothing else: it never takes the
	// keyboard, never takes a click, and the application never activates.
	//
	// It is for a window that SHOWS something rather than one somebody works in --
	// a head-up display, a viewer on a second screen, a surface inside glasses.
	// Such a window taking input is not a missing feature, it is a TRAP: it takes
	// the keyboard from whatever the person was typing into, and the pointer that
	// wanders onto the display it owns is invisible, because the picture is of
	// somewhere else and does not show where the mouse is. That was measured on a
	// pair of glasses, and the way out was unplugging them.
	//
	// A passive window is driven from outside instead -- a global shortcut, a
	// menu-bar item, a socket -- so an application that asks for this must have
	// another way in. Its own loop still runs: it repaints, resizes and closes as
	// before, and nothing about drawing changes.
	//
	// Honoured today by the macOS (Cocoa) back-end; elsewhere it is accepted and
	// ignored.
	Passive bool

	// Immersive puts the window above the platform's own furniture -- on macOS
	// the menu bar and the Dock -- instead of underneath it.
	//
	// [Config.Fullscreen] covers the desktop and nothing more: the menu bar and
	// the Dock are drawn at window levels above an ordinary window, so on a
	// display that carries them they appear ON TOP of the picture. For a
	// surface showing captured desktops that reads as two menu bars, one of them
	// belonging to a screen the viewer is not looking at.
	//
	// It is a window level rather than a presentation option deliberately:
	// presentation options apply only while this application is active, and an
	// immersive surface driven by global shortcuts is used precisely while
	// another application has the keyboard.
	//
	// Honoured today by the macOS (Cocoa) back-end; ignored elsewhere.
	Immersive bool
}

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 Numbered added in v0.56.0

type Numbered interface {
	Number() uint32
}

Numbered is an optional Backend capability: the window's system-wide identifier.

On macOS that is -[NSWindow windowNumber], which IS the CGWindowID that CoreGraphics and ScreenCaptureKit use. It exists so a program can tell a CAPTURE about its own window: capturing a display this window sits on otherwise feeds the window back into itself -- an overlay filming the screen it covers -- and the capture API takes a list of window ids to leave out.

Zero means the back-end has no such number, or has not got one yet.

Implemented today by the macOS (Cocoa) back-end.

type Placement added in v0.51.0

type Placement interface {
	Bounds() (x, y, width, height int, ok bool)
}

Placement is an optional Backend capability: where the window actually is.

Asking for a display is not the same as getting one. A back-end resolves Config.Screen against the displays attached at that moment, the platform may have its own opinion about where a window may sit, and a desktop that is rearranged while an application is starting can move the panel out from under the rectangle it was about to be placed at. Bounds is how an application that must own a particular display -- an XR headset, a kiosk, a presentation -- can check rather than assume, and it is how this package's own live tests assert placement from metadata instead of from pixels.

The rectangle is in the same global top-left coordinates Screen reports, so a caller compares it against the Screen it asked for directly. ok is false on a back-end that can open a window but cannot say where it went.

Implemented today by the macOS (Cocoa) back-end.

type Repainter added in v0.17.0

type Repainter interface {
	Repaint()
}

Repainter is an optional Backend capability: ask for a repaint from ANY goroutine.

This package repaints when something happens — an event, a resize, a [scene.HostRoot] invalidation — which covers an interface that only changes because the user did something. It does not cover an application whose content arrives on its own: a feed reader with a fetch in flight, a log viewer, a clock. Such an application draws its first frame and, with the window idle, would show it for as long as it runs.

Repaint is safe to call from any goroutine and returns immediately; the back-end marshals the work to whatever thread its platform demands. Calling it more often than the display refreshes is not an error, just wasted frames.

Implemented by every back-end that has a run loop to wake: X11 (a SendEvent to its own window), Wayland, GTK, macOS (Cocoa), Windows (Win32), and the browser tab (the next animation frame).

An application whose results arrive on other goroutines posts them to an mvvm.Queue whose wake is Repaint, and drains the queue at the start of a frame -- in its root container's SetBounds, which every back-end calls before drawing. Repaint wakes the loop; the queue is what makes the change safe.

type Scaler added in v0.16.0

type Scaler interface {
	RenderScale() float64
}

Scaler is an optional Backend capability: how many framebuffer pixels the back-end is allocating per logical point.

It is the answer to Config.RenderScale, which may have been NativeScale -- "whatever the panel is" -- and so is not something the caller can compute from what it passed in. A self-rendering root needs it to tell its own renderer what a point is worth: Backend.Size reports FRAMEBUFFER pixels, and dividing by this gives the logical size the user actually sees.

A back-end that does not implement it renders one pixel per point.

type Screen added in v0.46.0

type Screen struct {
	// Name is the display's human-readable name, e.g. "Color LCD" or
	// "VITURE Beast". It is what to show a user choosing an output, and may be
	// empty on a display that publishes none.
	//
	// It is the panel's OWN name wherever the platform offers one and that name
	// identifies the display — on Linux the product string out of its EDID. It
	// falls back to the connector ("HDMI-1", "DP-2", "HEADLESS-1") when the
	// display publishes no product name, and ALSO when it publishes one that
	// two attached displays share: two identical monitors say the identical
	// thing about themselves, and a name that cannot tell them apart is not a
	// name.
	Name string
	// X, Y, Width, Height are the display's full bounds.
	X, Y          int
	Width, Height int
	// Visible* is the usable area, with the menu bar and Dock (or their
	// platform equivalents) excluded. On a secondary display it is normally the
	// full bounds.
	VisibleX, VisibleY          int
	VisibleWidth, VisibleHeight int
	// Scale is the display's backing factor: device pixels per logical point. A
	// Retina panel reports 2.
	Scale float64
	// Primary reports the display that owns the desktop's origin — the one
	// carrying the menu bar or task bar. Exactly one screen has it set.
	//
	// It is deliberately not "the active screen": which display holds the
	// focused window changes as the user clicks around, and a caller choosing an
	// output wants the stable answer.
	Primary bool
}

Screen describes one attached display, in LOGICAL points — the unit the toolkit lays out and the user reads in, not device pixels.

X and Y are a TOP-LEFT origin with Y growing downwards, so a screen sitting above the primary one has a negative Y. That is the convention the X11, Wayland and Win32 back-ends use; macOS's own space is bottom-left with Y growing up, and the Cocoa back-end converts. A caller therefore never has to know which platform it is on to reason about the layout of the desktop.

Placement does not go through these numbers. Pass the Screen value itself back through Config.Screen and the back-end re-resolves it against the displays attached at that moment, so a window lands on the panel the caller picked rather than at coordinates that may have stopped describing it.

func (Screen) IsZero added in v0.46.0

func (s Screen) IsZero() bool

IsZero reports whether s names no display, which is what a caller gets from the zero value.

type ScreenList added in v0.75.0

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

ScreenList is the display list of a live system: NEVER EMPTY, primary first, and with exactly one primary — by construction rather than by convention.

⛔⛔ BOTH HALVES WERE CONVENTIONS, AND ONE OF THEM COST SOMEBODY THEIR DESK. Screens used to answer []Screen, so nothing stopped an empty one arriving, and the darwin back-end sent one: liveDisplays returned (nil, nil) when the window server counted zero, which is a failed read wearing the clothes of a fact. go-xrkit/desk looks its display up by name, found none, and quit -- twice on 2026-09-08, on a headset somebody was wearing:

desk: "VITURE Beast" is not attached any more; there is  -- stopping

Nothing after "there is": the message names every attached display and named NONE. Three of the four back-ends already refused to say it (Windows and Wayland returned an error, and primaryBounds called it ErrDisplayList); the fourth was the one that shipped.

⭐ AND THE PRIMARY WAS A REPAIR RATHER THAN A GUARANTEE. primaryFirst walked the slice unsetting duplicate flags -- "only one may claim it" -- and every caller that wanted the main display then scanned for the flag again. It is a field of the list, not a property to go looking for.

The zero ScreenList is empty and reports itself so; there is no way to build a populated one but [newScreenList], which is where both rules live.

func Screens added in v0.46.0

func Screens() (ScreenList, error)

Screens enumerates the attached displays, primary first, in LOGICAL points. It is safe to call before Open — picking an output is something an application does on the way in.

It asks the SAME display server Open would dial, and by the same rule: Wayland when $WAYLAND_DISPLAY is set, X11 otherwise. Anything else would let a caller pick a display off one server and open a window on another.

See Screen for what the fields mean; the two back-ends fill them from very different protocols and are documented where they do it (screen_wayland.go and screen_x11.go).

func (ScreenList) All added in v0.75.0

func (l ScreenList) All() []Screen

All is every screen, primary first.

The slice is the list's own: reading it is free, and writing to it would break the guarantees this type exists for. Copy it if you mean to sort it.

func (ScreenList) ByName added in v0.75.0

func (l ScreenList) ByName(name string) (Screen, bool)

ByName is the screen called name, and whether there is one.

⭐ IT IS HERE BECAUSE EVERY CALLER WROTE IT. Looking a display up by name is what survives the desktop being rearranged — creating a virtual display moves the others, so a rectangle captured a moment ago names nothing — and three separate loops were doing it.

func (ScreenList) Len added in v0.75.0

func (l ScreenList) Len() int

Len is how many screens there are. It is at least 1 for any list a back-end returned, and 0 only for the zero value.

func (ScreenList) Primary added in v0.75.0

func (l ScreenList) Primary() Screen

Primary is the display that owns the desktop's origin.

⭐ A FIELD RATHER THAN A SEARCH. Every caller that wanted it used to loop looking for Screen.Primary, which is a scan for something the list already knew — and a scan that has to decide what to do when it finds none.

The zero value has no screens and returns the zero Screen, which Screen.IsZero reports.

type Visibility added in v0.86.0

type Visibility interface {
	Show() error
	Hide() error
	Raise() error
}

Visibility is an optional Backend capability: take an open window off the screen without closing it, put it back, and bring it to the front.

It is what an application with a system-tray icon needs. Its "Open" item raises the window that is already there rather than opening a second one, and its window can be hidden while the application carries on in the tray. Hiding is not closing: Backend.Run keeps running, the widget tree keeps its state, and Show puts back exactly what was there.

All three are safe from any goroutine -- a tray's menu runs on its own -- and return once the request is on its way to the window's own thread, without waiting for the platform to act on it.

  • Show makes a hidden window visible again. It does not ask for the focus, and does nothing to a window that is already visible.
  • Hide takes the window off the screen, and out of the taskbar or Dock where the platform lists windows there.
  • Raise shows the window if it is hidden, restores it if it is minimised, brings it to the front and asks for the keyboard focus. The platform may refuse the focus -- focus-stealing prevention is a desktop's prerogative -- and Raise does not report that, because nothing tells it.

Implemented by the X11, macOS (Cocoa) and Windows (Win32) back-ends. On Wayland, Show and Hide are implemented and Raise answers ErrNotSupported: xdg-shell gives a client no request to raise or focus its own window. The wasmbox, Android and GTK back-ends do not implement it; Show, Hide and Raise answer ErrNotSupported for them.

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) Appearance added in v0.22.0

func (w *Window) Appearance() Appearance

Appearance reports the desktop's colour scheme and accent colour. Implements the AppearanceReader capability.

func (*Window) ClipboardText added in v0.21.0

func (w *Window) ClipboardText() string

ClipboardText asks the current owner for the selection and waits for it.

Implements the Clipboard capability.

func (*Window) Close

func (w *Window) Close() error

Close closes the window's connection to the server.

Safe from any goroutine: the shared segment is detached under fbmu, so a run loop mirroring pixels into it finishes its frame first and finds the window closed on the next one.

func (*Window) Hide added in v0.86.0

func (w *Window) Hide() error

Hide withdraws the window (ICCCM 4.1.4): it leaves the screen and, under a window manager, the taskbar, rather than becoming an icon there.

func (*Window) Raise added in v0.86.0

func (w *Window) Raise() error

Raise maps the window, puts it on top of the stack, and asks the window manager to activate it. The stacking request is what a window manager without EWMH -- or no window manager at all -- still honours; _NET_ACTIVE_WINDOW is what brings a minimised window back and gives it the focus under one that speaks EWMH.

func (*Window) RenderScale added in v0.36.0

func (w *Window) RenderScale() float64

RenderScale reports how many framebuffer pixels this window allocates per logical point. Implements the Scaler capability.

It is 1 unless the caller asked for NativeScale and the desktop published an Xft.dpi that says otherwise.

func (*Window) Repaint added in v0.32.0

func (w *Window) Repaint()

Repaint asks the run loop for a frame. Implements the Repainter capability; safe to call from any goroutine, returns without waiting for the frame.

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) SetClipboardText added in v0.21.0

func (w *Window) SetClipboardText(text string)

SetClipboardText claims the CLIPBOARD selection and remembers the text, which is all copying is here. The text is handed out later, one requestor at a time, by answerSelectionRequest.

Implements the Clipboard capability.

func (*Window) Show added in v0.86.0

func (w *Window) Show() error

Show maps the window again.

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.

func (*Window) SystemFontTTF added in v0.22.0

func (w *Window) SystemFontTTF() ([]byte, error)

SystemFontTTF reports that there is no font file to hand over. Implements the AppearanceReader capability.

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
atspi
Package atspi is the Linux accessibility bridge: it publishes the widget tree on the AT-SPI bus, where Orca and every other Linux screen reader read it.
Package atspi is the Linux accessibility bridge: it publishes the widget tree on the AT-SPI bus, where Orca and every other Linux screen reader read it.
capture
Package capture answers one question for the live suites: where may a picture of somebody's screen be written?
Package capture answers one question for the live suites: where may a picture of somebody's screen be written?
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.
dnd
Package dnd is the backend-agnostic drag-and-drop state machine that every native windowing backend shares.
Package dnd is the backend-agnostic drag-and-drop state machine that every native windowing backend shares.
gtk
Package gtk is the GTK4-hosted Linux back-end: instead of blitting to a from-scratch X11/Wayland surface, GTK owns the window, the toolkit's pixel framebuffer goes in a GtkPicture, and native platform controls (toolkit.NativeControl) are real GtkEntry/GtkButton/… overlaid above it in a GtkFixed.
Package gtk is the GTK4-hosted Linux back-end: instead of blitting to a from-scratch X11/Wayland surface, GTK owns the window, the toolkit's pixel framebuffer goes in a GtkPicture, and native platform controls (toolkit.NativeControl) are real GtkEntry/GtkButton/… overlaid above it in a GtkFixed.
nativeclip
Package nativeclip works out the geometry of showing only the part of a native control that its viewport still shows.
Package nativeclip works out the geometry of showing only the part of a native control that its viewport still shows.
tab
Package tab runs a widget tree in a plain browser tab, on a <canvas>, through go-widgets/webcanvas: no compositor, no SharedArrayBuffer, no cross-origin isolation.
Package tab runs a widget tree in a plain browser tab, on a <canvas>, through go-widgets/webcanvas: no compositor, no SharedArrayBuffer, no cross-origin isolation.
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

Jump to

Keyboard shortcuts

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