application

package module
v0.2.0 Latest Latest
Warning

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

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

README

application — cross-platform application lifecycle for go-widgets

CI Go Reference

The layer above go-widgets/window: where window presents a single surface, application owns the whole app lifecycle. It opens the native OS window, drives its run loop, blits the handler's own RGBA framebuffer into it, composes a system-tray / menu-bar icon alongside the window (via go-widgets/tray), threads the live system appearance (dark/light, accent, system font) into the handler, translates native input into the toolkit's event vocabulary, publishes an accessibility tree, and fires a ready callback once the first frame is on screen. No WebKit, no wasm, no HTTP — every widget is drawn by the framework and the window is just a bitmap surface plus a native event source. Everything is CGO_ENABLED=0.

Opening a real OS window and pumping its event loop is a launch-verified boundary, so the native Run path is excluded from the coverage gate (like the wasm glue elsewhere); the contract, the event translation, the present gating, the appearance pump, the launch-height seam and the ready-after-first-frame counter are all unit-tested to 100%.

platform native window (go-widgets/window) native tray (go-widgets/tray)
darwin Cocoa / NSWindow (purego + the Obj-C runtime) NSStatusItem + NSMenu
windows win32 (golang.org/x/sys/windows syscalls) Shell_NotifyIcon + TrackPopupMenu
linux X11 / Wayland (pure-Go wire protocols) StatusNotifierItem over DBus

The tray's native backends are opt-in via the tray_native build tag; without it the tray is a harmless no-op and the window still runs, so a headless build or a platform without native tray support degrades gracefully.

Usage

package main

import (
	"github.com/go-widgets/application"
	"github.com/go-widgets/tray"
)

func main() {
	spec := application.Spec{
		Name:       "News Reader",
		Identifier: "com.example.reader",
		Version:    "1.0.0",
		Icon:       iconPNG, // []byte, optional
		Tray: func() *tray.Menu { // optional
			return tray.NewMenu().Add(
				tray.Item("Refresh", func() { app.Refresh() }),
				tray.Item("Quit", func() { app.Quit() }),
			)
		},
	}

	cfg := application.Config{Title: "News Reader", Width: 1200, Height: 800}

	// handler renders the app's own framebuffer and takes its input; onReady
	// fires once, after the first frame is visible.
	err := application.Run(spec, cfg, handler, func() {
		log.Println("first frame is on screen")
	})
	if err != nil {
		log.Fatal(err)
	}
}

handler implements application.Handler (Frame / Resize / MouseDown / MouseMove / MouseUp / Scroll / Key). It may additionally implement any of the optional capability interfaces — AppearanceSink, ShortcutSink, SecondaryClicker, ContextMenuHost, ClipboardController, Accessible — and the run loop, which never wraps the handler, will honour each one it finds.

For a non-window host (a desktop shell, wasmdesk, a tab in something larger), Bind(handler, scale) and BindScaled(handler, scaleOf) return a *toolkit.Surface wired to the same handler without opening a window.

License

BSD-3-Clause. See LICENSE.

Documentation

Overview

Package application owns a go-widgets app's whole lifecycle: it opens the native OS window, runs its event loop, blits the handler's own RGBA framebuffer into it, composes a system-tray / menu-bar icon alongside it, and fires a "ready" callback once the first frame is on screen. It is the sibling of go-widgets/window: window presents a single surface, application is the layer above it that turns a handler plus an identity (name, icon, tray) into a running application. There is no WebKit, no wasm, no HTTP — every widget is drawn by the framework (toolkit.Surface) and the window is just a bitmap surface plus a native event source. Everything is CGO_ENABLED=0.

Opening a real OS window and pumping its event loop is inherently a launch-verified boundary, so the Run path through go-widgets/window is excluded from the coverage gate (like the wasm glue elsewhere); this contract, the event translation, the present gating, the appearance pump, the launch-height seam and the ready-after-first-frame counter are all unit-tested.

The optional capability interfaces (AppearanceSink, ShortcutSink, SecondaryClicker, ContextMenuHost, ClipboardController, Accessible) are the reason the run loop must never WRAP a Handler: a wrapper would satisfy the bare Handler interface and silently drop every capability the concrete handler also implements. Everything here threads the ORIGINAL handler through, and type-asserts for a capability at the point of use.

Index

Constants

This section is empty.

Variables

View Source
var ErrUnsupported = errors.New("application: no native window back-end on this platform")

ErrUnsupported is returned by Run on a platform without a native back-end.

Functions

func Bind

func Bind(h Handler, scale float64) *toolkit.Surface

Bind returns a toolkit.Surface showing h: the frame it presents, the input it takes, and the tree it publishes for a screen reader.

Run uses it to fill a native window, but it is exported because a window is not the only place this can go. Any go-widgets host that can lay out a widget can host this application — a desktop shell, a tab in something larger, wasmdesk — and none of them should have to reimplement the translation.

scale is the framebuffer pixels per logical point the host is rendering at; pass 1 if it does not scale. A host whose scale CHANGES while running — a window dragged between displays of different density — should use Run, which re-reads it every frame.

It is also what makes the wiring testable. Everything specific to the app lives here — the resize units, the event translation, the element mapping — and none of it needs a window to be wrong, so a test can drive a real app scene through a real surface and assert the scene moved.

func BindScaled

func BindScaled(h Handler, scaleOf func() float64) *toolkit.Surface

BindScaled is Bind for a host whose scale CHANGES while it runs: a window dragged between displays of different density. The function is called every frame, and the handler is told about a change the moment it happens.

func Run

func Run(s Spec, cfg Config, h Handler, onReady func()) error

Run opens the application's window and runs it to completion, putting up a menu-bar tray (when Spec.Tray is set) attached to the window's run loop, and invoking onReady once after the first frame is shown. It mirrors window.Run but owns the whole app lifecycle. (The .app bundle + Dock icon are a build-time concern handled by the packager, not here.)

The tray is ATTACHED, not Run: attaching joins the platform's already-running main loop (the one win.Run drives for the window) rather than starting a second one, which is the only way a window and a tray can coexist in one process. tray.Attach must be called on the platform's main/UI thread and, per its contract, may block until the tray is torn down, so it is started on its own goroutine and stopped with Quit when the window returns — whichever way the window loop ends (a clean quit, or an error), the deferred Quit tears the tray down so no menu-bar icon is orphaned.

A backend that cannot attach a tray (a headless build, or a platform without native tray support) makes tray.Attach return an error, which is deliberately swallowed: a missing tray must not stop the window from running. The window is the application; the tray is a convenience on top of it.

Types

type A11yElement

type A11yElement struct {
	Role       string
	Name       string
	Value      string
	X, Y, W, H int
}

A11yElement is one element the handler wants the platform's accessibility layer to expose: what it is, what it says, and where it sits.

Rect is in DEVICE PIXELS with a top-left origin — the same space Frame's buffer and the MouseDown coordinates use — because that is the only space this package and its handler already agree on. Each back-end converts it to whatever its own accessibility API wants (macOS: screen points, y-up).

Role is a neutral name, not a platform constant: this package presents pixels and must not drag a platform's vocabulary into the handler. The back-end maps it (see axRole).

type Accessible

type Accessible interface {
	// A11yElements returns the current elements in reading order. It is called
	// from the platform's accessibility client, which on macOS is the main
	// thread, at unpredictable times — the implementation must be safe to call
	// between frames.
	A11yElements() []A11yElement
}

Accessible is implemented by a Handler that can describe what it is showing. A back-end that supports an accessibility API asks for the description when the platform requests it; a handler that does not implement this simply presents pixels, as before.

It is a separate, optional interface rather than a Handler method so that adding it breaks no existing back-end or handler.

type AppearanceSink

type AppearanceSink interface {
	SystemAppearance(SystemAppearance)
}

AppearanceSink is an optional Handler capability. A back-end that can read the host appearance (currently the macOS Cocoa back-end) pushes it so the UI adopts the native dark/light mode, accent colour, and system font.

type ClipboardController

type ClipboardController interface {
	SetSystemClipboard(SystemClipboard)
}

ClipboardController is an optional Handler capability. A back-end that can reach the platform pasteboard installs its SystemClipboard here at startup (the mirror of AppearanceSink: this one flows a capability OUT to the handler). A back-end without clipboard support installs nothing, leaving the toolkit's default in-process clipboard in place (copy/paste still work within the app, just not across the OS).

type Config

type Config struct {
	Title         string
	Width, Height float64
}

Config controls the window.

type ContextMenuHost

type ContextMenuHost interface {
	ContextMenuActive() bool
	ContextMenuEvent(ev toolkit.Event)
}

ContextMenuHost is an optional Handler capability: while it reports a context menu open, the window feeds every input event to ContextMenuEvent instead of the usual MouseDown/Move/Scroll/Key path, so the menu is modal. The handler closes the menu itself (an item fired, a click landed outside, Escape) and then reports it inactive again.

type Handler

type Handler interface {
	// Frame returns the current RGBA framebuffer (w*h*4 bytes) and whether it
	// changed since the last call (damage gate).
	Frame() (buf []byte, w, h int, changed bool)
	// Resize maps the new logical size to device pixels; scale is the backing
	// scale factor (device pixels per point).
	Resize(w, h int, scale float64)
	// MouseDown reports a left button press at device-pixel coordinates.
	MouseDown(x, y int)
	// MouseMove reports pointer motion at device-pixel coordinates. It fires
	// continuously during a left-button drag (back-ends need not emit idle
	// hovers) so the handler can drive interactions like a divider resize.
	MouseMove(x, y int)
	// MouseUp reports a left button release at device-pixel coordinates.
	MouseUp(x, y int)
	// Scroll reports a wheel delta in device pixels.
	Scroll(dy int)
	// Key reports a key press: name is a symbolic label for editing keys
	// ("Backspace"/"Escape"/"Enter"), r the rune for a printable character.
	Key(name string, r rune)
}

Handler is the presenter's data source and input sink. The window calls Frame each tick (and after each event) and blits the returned buffer only when it reports changed. Input coordinates are device pixels (points × backing scale) with a top-left origin, matching the framebuffer.

type NativeControlProvider added in v0.2.0

type NativeControlProvider interface {
	// NativeControls returns the native controls to embed this frame, keyed for
	// identity across frames, in the framebuffer's device-pixel, top-left space
	// (like A11yElements and MouseDown). It is called once per frame from the
	// back-end's main thread; a control the app is not showing this frame is
	// simply omitted.
	NativeControls() []toolkit.NativeControl
}

NativeControlProvider is implemented by a Handler that wants some of what it draws backed by real OS controls — a secure text field, a button, a slider — embedded over its framebuffer. A back-end that can host native controls asks for the descriptors each frame and reconciles the live controls to them; a handler that does not implement this simply presents pixels, as before.

It is a separate, optional interface, the exact parallel to Accessible: where Accessible describes the surface for the platform's accessibility layer, this describes the parts of it that should be real controls. The descriptor is the toolkit's own toolkit.NativeControl — kind, key, geometry, value, and the callbacks that carry the person's input back — because the back-end's seam (toolkit.Surface.Controls) speaks exactly that.

type SecondaryClicker

type SecondaryClicker interface {
	SecondaryClick(x, y int)
}

SecondaryClicker is an optional Handler capability: a secondary (right / two-finger / Control-click) press arrives here as the context-menu gesture, with the same device-pixel coordinates as a MouseDown. A handler that has no context menu simply does not implement it, and the press is ignored.

type ShortcutSink

type ShortcutSink interface {
	Shortcut(r rune, ctrl, meta bool)
}

ShortcutSink is an optional Handler capability: a key pressed with a command-style modifier (Ctrl/Cmd) arrives here as a shortcut, r is the base rune (modifiers stripped), ctrl/meta report which modifier was held. Back-ends route a modifier chord here instead of through Key (which drops the modified rune) so the app can act on real-browser-style shortcuts like Cmd+=.

type Spec

type Spec struct {
	Name       string            // e.g. "News Reader" — tray tooltip
	Identifier string            // reverse-DNS, e.g. "com.example.app"
	Version    string            // e.g. "1.2.0"
	Icon       []byte            // PNG icon bytes for the tray (optional)
	Tray       func() *tray.Menu // optional: builds the menu-bar menu
}

Spec is the application's identity and its optional tray. It is the piece that separates an application from a bare window: a name and identifier the OS and the tray tooltip can show, a version, and — when the app wants a menu-bar / system-tray presence — an icon and a builder for its menu.

type SystemAppearance

type SystemAppearance struct {
	// Dark is the effective dark/light mode (macOS effectiveAppearance).
	Dark bool
	// Accent is the user's accent colour (macOS controlAccentColor); only
	// meaningful when HasAccent is set.
	Accent    color.RGBA
	HasAccent bool
	// FontTTF is the raw system font (e.g. macOS SFNS.ttf). Empty on a poll that
	// only refreshes colours, so the already-installed font is kept.
	FontTTF []byte
}

SystemAppearance carries look-and-feel harvested from the host UI so the renderer can match the live system look rather than a fixed palette.

type SystemClipboard

type SystemClipboard interface {
	ClipboardText() string
	SetClipboardText(text string)
}

SystemClipboard is a host OS text clipboard (read + write). Its method set is deliberately identical to the toolkit's back-end-neutral Clipboard interface, so the app can install a back-end's SystemClipboard as the toolkit-wide clipboard directly — making every text widget's copy/cut/paste, and the app's copy actions, go through the real OS pasteboard.

Jump to

Keyboard shortcuts

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