Reference
MyGo architecture
How MyGo works inside, for contributors, with a table of the differences between platforms.
This guide explains how MyGo is put together: the layers, the threading
model, how Go talks to the native toolkits without cgo, how pages and Go
exchange typed messages, how MyGo draws native UI, and how to extend the
framework safely. Read it before changing anything under internal/.
Goals and constraints
- Low overhead. A hello-world app is a ~7 MB binary with a ~35 MB
physical footprint on macOS (mostly AppKit/WebKit), ~62 MB with the
processes WKWebView runs for its page, GPU and network, and idles at 0%
CPU. A window of native UI starts no webview process: idle, a small one
takes 44 MB on macOS, and the counter example 65 to 67 MB on Linux, tiled
to half of a 4K display at scale 2, 15 MB of it the window's buffer,
without loading Mesa (122 to 155 MB with it). Nothing
polls: all work is driven by native events or explicit wake-ups. A
window of native UI whose content settled asks for no frame and arms no
timer (
TestSettledWindowSleeps): the display link pauses, Go's scheduler sleeps, and the counter example idles at about 0.005% of a core on macOS (an AppKit window, 0.002%) and wakes no thread under X11. - No cgo. Everything builds with
CGO_ENABLED=0, so any platform can be cross-compiled from any machine. Native APIs are called at run time through purego (dlopen+ assembly trampolines) on macOS and Linux and through thesyscallpackage on Windows, never throughimport "C". - Two kinds of windows. Web pages show in the system webview:
WKWebView on macOS, WebKitGTK 4.1 (4.0 as a fallback) on Linux, WebView2
on Windows (amd64 and arm64); no browser engine is bundled. Native UI is
drawn by MyGo itself, in Go, with Metal, Direct3D 11 or OpenGL, or on the
CPU, and text from the system's own engines (see Native UI);
an app whose windows all show it needs no webview at all, and runs in
browsers too, compiled to WebAssembly, on canvases drawn with WebGL 2
(see Browsers). On unsupported
platforms, 32-bit Windows and Linux among them, the
internal/unsupportedbackend makesApp.Runfail with a clear error while everything still compiles, so every file of a backend, and of the text engine it pairs with, names the backend's architectures in its build constraint. - Bun is dev tooling only. It builds and tests the TypeScript bridge, and installs and runs the web template's tools (Vite, TypeScript, the mygo-cli package); projects of native UI need none. Nothing Bun-related ships in an app.
- Great DX over API parity. The API has the feel of Electron (app
lifecycle, windows, menus, dialogs) but is Go-first: typed IPC with a
generated TypeScript client,
http.Handlerfor custom protocols, typed event structs, blocking calls that are safe from any goroutine, and native UI written in Go alone.
Repository layout
. package mygo: the public API
├── app.go lifecycle, quit sequence, Dock, paths (paths.go)
├── window.go Window: the native window, its options, state and events
├── flyout.go NewFlyout: windows owned by a parent, placed next to an anchor
├── page.go Page: the web page a window shows, its loading, Eval and events
├── content.go Content: windows showing native UI instead of a page
├── webview.go WebView: web pages in windows of native UI, under it
├── ipc.go Bind/BindAs, method calls, Event[T], CallerWindow
├── plugin.go Plugin and Use: services bound as "plugin:<name>"
├── channel.go Channel[T]: values streamed to a call's page
├── typescript.go GenerateTypeScript / WriteTypeScript (uses internal/tsgen)
├── protocol.go custom schemes served by http.Handler, FileServer
├── frontend.go the app's frontend: relative URLs, devUrl, mygo://localhost
├── menu.go Menu/MenuItem model, roles, native item updates
├── cookies.go Cookies: the pages' cookies, checked, keyed by name, domain and path, filtered by CookieFilter
├── session.go Session: website data stores chosen per page (PageOptions.Session), ClearData by kind
├── recent_docs.go jumplist.go: recent documents (Open Recent and the system's lists), the jump list
├── dialog.go modules.go clipboard.go: shell, clipboard, screen, theme, tray, shortcuts, notifications
├── watch.go watch_scan.go: WatchFiles, and what a watch saw of its directory
├── document*.go NewDocuments: documents, their saving, recovery copies, watches, menus
├── loop.go main-thread queue: postMain / onMain / await
├── events.go listener lists and the Preventable event types
├── single_instance.go RequestSingleInstanceLock over a Unix socket
├── dev.go signal_*.go IsDev, the `mygo dev` ready signal, quitting on SIGINT/SIGTERM
├── backend_*.go picks the backend per GOOS
├── internal/
│ ├── platform/ the contract every backend implements
│ ├── darwin/ macOS: AppKit + WKWebView through the Objective-C runtime
│ ├── linux/ Linux: GTK 3 + WebKitGTK through dlopen
│ ├── windows/ Windows: Win32 + WebView2 through syscall and COM
│ ├── schemehost/ maps custom scheme URLs to the http://*.localhost URLs WebView2 serves them at
│ ├── schemestream/ the bounded pipe custom scheme bodies stream to WebView2 through, and media ranges narrowed
│ ├── web/ browsers (js/wasm): native UI on canvases, through syscall/js
│ ├── unsupported/ stub for other platforms
│ ├── fake/ in-memory backend for unit tests
│ ├── automationcli/ mygo-automation, the command line client of plugins/automation's socket
│ ├── watchdriver/ the goroutine that runs a backend's file watcher
│ ├── bridge/ embeds bridge.js, built from packages/bridge
│ ├── surface/ the connection between a window and its native UI
│ ├── pacing/ whether frames wait for the display (MYGO_UNCAPPED)
│ ├── scene/ display lists and glyph atlases, what renderers draw
│ ├── media/ the contract between ui's VideoPlayer and ui/video's engines
│ ├── text/ fonts, shaping, line breaking, editing, glyph rasterization
│ ├── rope/ the persistent tree of text chunks text buffers keep
│ ├── diff/ line and word diffs: histogram, Myers, git's indent heuristic
│ ├── vec/ the coverage rasterizer of glyphs and paths
│ ├── raster/ the CPU renderer of scenes
│ ├── gpu/ the instances GPU renderers draw, and gputest/ for their tests
│ ├── gpu/d3d11/ the Direct3D 11 renderer of scenes, and device/ for its devices
│ ├── gpu/metal/ the Metal renderer of scenes
│ ├── gpu/gl/ the OpenGL and WebGL 2 renderer of scenes
│ ├── shader/ the compiler of shaders in Go's syntax, forked from Ebitengine's Kage
│ ├── shaderir/ its IR; glsl/, hlsl/ and msl/ write it for the GPU renderers, interp/ runs it on the CPU
│ ├── svg/ SVG parsing and drawing, for icons and images
│ ├── tsgen/ TypeScript client generator
│ ├── nativegen/ native modules: reads their Go interfaces, writes their Go, Swift, Rust and Zig
│ ├── i18ngen/ checks catalogs of messages, writes their Go and TypeScript functions
│ ├── i18nlink/ what package i18n gives MyGo's own catalog only: a catalog that links no dates
│ ├── l10n/ MyGo's own strings: the catalog of the controls and menu roles
│ ├── nativelib/ mygo-plugin.json, libraries' file names, where they are built and cached
│ ├── accelerator/ parses "CmdOrCtrl+Shift+K"
│ ├── menuicon/ menu items' PNG icons as premultiplied, scaled pixels (Windows)
│ ├── update/ update manifests, signatures, archives and delta updates
│ ├── idlemem/ measures apps' memory once idle, for the benchmarks
│ ├── inputlatency/ measures how long input takes to reach the screen in native UI (macOS)
│ └── e2e/ GUI tests and benchmarks against the real backend (MYGO_E2E=1)
├── packages/ Bun workspace (with the examples' frontends):
│ ├── bridge/ the runtime injected into pages (→ internal/bridge/bridge.js)
│ ├── runtime/ mygo-runtime, the npm package apps and generated clients import
│ └── cli/ mygo-cli, the npm package of the CLI, and in npm/ its
│ per-platform binary packages
├── plugins/ official plugins, each a Go package and its npm
│ package (@mygo-plugins/<name>) side by side: fetch,
│ websocket, sqlite, watch; and Go only: updater, the
│ update window, a web page or native UI (updater/native),
│ terminal, a view of native UI running programs with
│ libghostty-vt, glass, game, what games need
│ beyond what apps do (game/gamepad), loading,
│ loading.dev's indicators for native UI,
│ automation, a socket other processes drive the
│ app through, for end-to-end tests, with its
│ command line client for agents
│ (automation/cmd/mygo-automation), and
│ permissionflow, the system's permissions and a
│ guide to System Settings, after PermissionFlow
├── ui/ native UI: views, layout, widgets, text editing, Tester;
│ ui/chart: charts without a look, on ui's public API;
│ ui/editor: the code editor and the diff viewer;
│ ui/shader: shaders in Go's syntax, on every renderer;
│ ui/video: video with the system's media framework
├── i18n/ languages: matching, CLDR plural rules and parent locales,
│ formatting of numbers and dates (generated by
│ i18n/internal/gen), ICU MessageFormat, catalogs
├── ffi/ native libraries (Rust, Zig, C) that packages name in
│ mygo-plugin.json: finding, loading and binding them
├── transfer/ immutable data items, representations, lazy providers and drag effects
├── netproxy/ the proxy of the system's settings, for Go's HTTP clients
├── native/ the runtime of native modules: Go interfaces implemented in Swift, Rust or Zig
├── audio/ sound: players, the mixer, the output of each platform, WAV;
│ audio/vorbis and audio/mp3 decode Ogg Vorbis and MP3,
│ audio/mp4 MP4 (fragmented too) with ALAC, audio/aac
│ AAC, audio/flac FLAC
├── cmd/mygo/ the CLI: init, generate, dev, build, native build, doctor
├── examples/ hello, todo, frameless, native; counter-native, vibrancy,
│ effort-slider, flyout, menubar-flyout, piano, farm
│ and mayoi (games), code-editor and gallery (native
│ UI), timeline (motion on one playhead); webview (web
│ views in native UI); launcher (a Spotlight-like panel,
│ native UI); wind (a texture of glyphs lit by a
│ wind, two shaders of ui/shader); loading (the
│ loading plugin's indicators); video (a GIF
│ looping behind a window's content, a trailer
│ with controls over it); permissionflow
│ (the permissions, asked for and guided to);
│ native-module (native modules in Swift, called from native UI);
│ rust-native (a Rust crate the CLI builds, called
│ through ffi)
├── docs/ the user guides, the official plugins' pages
│ (plugins/), and this architecture guide
└── website/ the website, with these docs: TanStack Start, prerendered
and served by Cloudflare Workers as static assetsLayers
user code ──► package mygo ──► platform.Backend ──► darwin | linux | windows | web | unsupported
▲ │ ▲ │
│ └─ handlers ────┘ (AppHandler, └─ purego ─► AppKit/WebKit, GTK/WebKitGTK
│ WindowHandler)
page (JS) ◄── bridge.js ◄── Eval (batched) ─┘
└──── postMessage(JSON) ──► WindowHandler.Messagepackage mygoowns all behavior that is not platform specific: option defaults, the window registry, event listeners, the quit sequence, IPC routing and encoding, menus, protocol handling, trust checks. It never calls a native API directly.internal/platformis a small, synchronous contract: aBackend(event loop, window factory, menus, dialogs, clipboard, …), aWindow(chrome + webview), and two handler interfaces the core implements to receive events (AppHandler,WindowHandler). Options arrive fully defaulted; backends never invent policy.- Backends translate the contract to native calls. They keep native objects alive, map native callbacks back to Go objects and report events.
Keeping policy in the core is what lets internal/fake test almost all
behavior without a GUI, and keeps each backend a thin translation layer.
Threading model
Cocoa and GTK must be driven from the thread that started the process. The rules are:
- The main goroutine is locked to the main thread (
runtime.LockOSThreadinmygo.go'sinit), andApp.Runmust be called from it.Runinitializes the backend and blocks in the native event loop. Before that no backend is initialized (the Linux one has not even loaded GTK), yetmainalready runs on the main thread, whereonMaincalls functions directly, and generate mode never initializes it. So a public method that reaches the backend either keeps its value in the core until the backend can take it, for settings (SetActivationPolicythroughAppOptions,SetMenuonce ready,Theme.SetSource,Theme.SetGTKCSSandDock.SetMenuright afterInit), or starts withneedsApp, which panics on the main thread beforeRunwith the name of the call, rather than letting it crash on Linux, hang inawaitor answer wrong on macOS. Only calls that work beforeIniton every backend (Locale, packaging info, login items, URL schemes,IsOnBattery) do neither. - Every
platformmethod is called on the main thread, and every handler callback runs there. The backend never needs locks for its own state. - Every public method is safe from any goroutine.
loop.goprovides:postMain(fn)appends to a FIFO queue and callsBackend.Signal, which makes the backend callAppHandler.Dispatch(→loop.drain) on the main thread. macOS uses aCFRunLoopSourceadded in all common modes (so it fires while menus track, windows resize and modal panels run); Linux usesg_idle_add, deduplicated with an atomic flag.onMain(fn)runsfndirectly when already on the main thread, else posts it and waits. After shutdown posted work is dropped instead of deadlocking.await(ch)waits for an asynchronous native result. Off the main thread it is a plain channel receive. On the main thread it pumps native events withBackend.Stepuntil the value arrives; whoever produces the value callsdeliver, which sends and thenBackend.Wakes the loop. This is what makesPage.Eval,CapturePageand dialogs usable from event listeners without deadlocking. Nested runs of the dispatch source are expected and safe. A nested loop started inside a run loop observer is not: CFRunLoop never runs that observer again meanwhile, and in the one AppKit displays views and Core Animation commits in, no window, panel or sheet would show. So native UI's frames, whose handlers (Clicked) may show a dialog, draw outside it on macOS, but those AppKit asks for, as a resize's (drawSoonininternal/darwin/surface.go).
- Event listeners run on the main thread, synchronously, so cancelable
events (
OnClose,OnBeforeQuit,OnWillNavigate, …) can answer the native toolkit immediately. IPC calls run on their own goroutines, so bound methods may block. - Validate in the caller's goroutine. Anything that can panic on bad
input (for example
WindowOptions.BackgroundColor) is checked before hopping to the main thread, so the panic points at the user's call.
Main-thread-only fields are marked as such in comments (for example
Window.native, Window.trusted). Fields shared with other goroutines are
guarded by a mutex or atomic.
File watches (watch.go) don't use the main thread: each runs on a
goroutine of its own, and the backend's watcher on another (see
Watching files).
Native interop without cgo
purego gives three primitives, used everywhere:
purego.SyscallN(fn, args...)for integer/pointer arguments. Fast, no reflection; it allocates only the array of its arguments, which escapes. Floats cannot be passed this way, nor structs, but for those C passes in integer registers, as Core Foundation'sCFRange, which goes as its two fields, and comes back asr1andr2.purego.RegisterFunc(&typedFn, addr)for signatures with floats or structs (NSRect,CGFloat,GdkRGBA*…). These are created once at startup; calling them uses reflection, so they are kept off hot paths.purego.NewCallback(fn)turns a Go function into a C function pointer. At most ~2000 callbacks can exist per process and they are never freed, so callbacks are created once per signature at startup and user data (a window id, a request id) identifies the target. Never create callbacks per window, per request or per call.
Package ffi gives apps and plugins the same for their own libraries (a
Rust crate, a Zig package, C): ffi.New(name, manifest) finds the library
of the package's embedded mygo-plugin.json ($MYGO_NATIVE_<NAME>, the
app's resources, the executable's directory and, outside packaged apps,
the CLI's builds in .mygo/native/<goos>-<goarch> of the working
directory's module, then the published file in the user's cache,
downloaded when missing), loads it once (dlopen with RTLD_LOCAL,
LoadLibrary on Windows), and Bind sets a struct's function fields with
purego.RegisterFunc, by tag or by the field's name in snake case, turning
purego's panics on unsupported signatures into errors. Bind and
BindFunc bind a pointer once, under the library's mutex, and remember
the result in a sync.Map, so that functions can call Bind before each
use from any goroutine without locking. The terminal and SQLite plugins
find their libraries through it. Elsewhere than macOS, Linux and Windows
on amd64 and arm64 it reports errors.ErrUnsupported. Apps that do not
import it link none of it, and production builds (mygo_nodownload, set
by mygo build with mygo_noinspector) leave out its downloader and
net/http with it, about 1.9 MB, as packaged apps never download.
macOS (internal/darwin)
objc.goloads the frameworks, caches selectors and classes, and wrapsobjc_msgSend:send(obj, "selector:", args...)for integer arguments and pre-registered typed variants (msgRect,msgSetFloat,msgInitWindow, …) for the rest. On amd64, methods returning structs larger than 16 bytes go throughobjc_msgSend_stret.- Classes are defined at run time with
objc.RegisterClass:MyGoWindow(borderless-friendlyNSWindow),MyGoWebView(records the lastmouseDown:for drag regions;window_drag.gofollows the press with a local event monitor:StartDragasks the window server (performWindowDragWithEvent:) while the pointer has not moved, and moves the window itself when it has, or when the window server did not begin, whichwindowWillMove:tells, not the window's frame: it lags),MyGoWindowDelegate(window, navigation, UI and script-message delegate in one object per window), the app delegate, scheme handler, menu and tray targets. Only protocols that exist at run time are adopted (WKScriptMessageHandleris not registered, and WebKit does not need it). - Overrides call super with
sendSuper(sendSuperSizefor anNSSize), naming the class they are defined in, never purego'sobjc.ID.SendSuper: that resolves super from the object's class, which key-value observing replaces with a generated subclass, so the override would call itself until the stack overflows.TestSuperFromDefinedClassrejects it. - The web view is not the content view. A plain
NSViewis, holding the web view and, with vibrancy, anNSVisualEffectViewbehind it. WebKit docks the inspector next to the web view in its superview; were that the window's frame view, AppKit would draw a broken legacy title bar from then on. Liquid Glass is anNSGlassEffectViewthere instead, which shows what is under it in the window rather than behind it: an opaque window is made clear while it shows one, and so is the content view a rounded window's color went to (clearForGlass), the color given back once the glass goes. - Memory is managed by hand. Objects created with
alloc/initare owned (+1) and must be released; convenience constructors return autoreleased objects. Code that creates temporary objects runs insidewithPool, which keeps the goroutine on its thread until it pops the pool: a pool belongs to the thread that pushed it, and a goroutine other than the main one, as those reading the bundle forApp.Name, may otherwise resume on another thread, where popping it crashes. Delegates and windows are released withautoreleasefromwindowWillClose:because AppKit still uses them while closing. - Blocks. Completion handlers passed to Apple APIs are created with
objc.NewBlockand released right after the call (the callee copies them). Blocks received from Apple (decision and completion handlers) are invoked withcallBlock; when they are called later they are_Block_copyd first and released after use. - Coordinates. AppKit's origin is the bottom-left of the primary screen;
MyGo's is the top-left.
rectToMac/rectFromMacconvert. - Quitting. Cmd+Q ends in
applicationShouldTerminate:, which runs the core quit sequence and then stops the run loop soApp.Runreturns (the reply isNSTerminateCancel; AppKit never callsexit). Quit Apple Events (Dock > Quit, AppleScript, logout) are handled by our own handler installed inapplicationWillFinishLaunching:, so senders get a success reply, or error -128 when a listener cancels.
Linux (internal/linux)
ffi.godlopens GLib, GObject, GIO, GDK, GTK 3, cairo, GdkPixbuf and, when installed, AppIndicator. WebKitGTK 4.1 (4.0), JavaScriptCore and libsoup 3 (2.4), which only windows showing web pages need, load with the first such window (webKit), or to clear browsing data: an app that shows only native UI never maps them and the 75 or so libraries they bring, some 7 MB and 20 ms of startup. Without them,errWebKitsays what to install when such a window is created. Symbols from newer WebKitGTK versions are bound optionally and feature-detected (webkitWebViewCallAsyncJavascriptFunction != nil).- Signals are connected with
g_signal_connect_data, passing the window id as user data;Backend.window(data)resolves it and ignores closed windows. GDK event structs are read at fixed 64-bit offsets (field[T]). - The main thread is the one that called
gtk_init_check;IsMainThreadcomparesgettid.Stepisg_main_context_iteration(NULL, TRUE)andWakeisg_main_context_wakeup. Initnames the program after its executable (g_set_prgname) beforegtk_init_check, which would name it afterargv[0]: GTK names the class of the windows and the app id after the program, as the desktop entry does (StartupWMClass), and WebKitGTK the directories of the pages' data. Started through a link of another name, aslinux.commandputs in/usr/bin, the app is then the same program.Boundsare those of the frame a reparenting X11 window manager puts around the window (gdk_window_get_frame_extents): GTK positions the frame (NORTH_WEST gravity) and sizes the window inside it, without the frame.ContentBoundsare where the content widget is (gdk_window_get_originplus its offset below the menu bar). The frame's extents are known once the window manager framed the window that shows, which its first synthetic configure report tells; until then a window expects the frame it had, or the one seen last around a decorated window that is resizable or not as it is (openbox: 1, 20, 1 and 5 px at the bottom, or 1). Wayland has no frame inBounds.- GTK geometry changes are asynchronous:
SetBoundsremembers the requested rectangle, within the sizes GTK gives the window (constrain), with the frame it expects, as does a window about to show, which X has where GTK created it, or where it was, until the window manager places it;Centermoves that rectangle along.Boundsreports that rectangle until a configure event reports its size and, on X11, its position, or the window manager's own (synthetic) report of the size comes, or another size, which the window manager or the user chose. A window being placed waits for the window manager's report: a reparenting one first puts its frame where it created it. If the frame is not the one expected, the rectangle grows with it around the size asked for inside it (SetContentBounds,UseContentSize, a window that showed before), or GTK is asked for the size inside the new frame. A report of the previous size was sent before the window manager took the request (openbox sends one when the size hints change); GTK would ask for that size again from it, so the backend asks for the new one again. - On Wayland, the configure event that activates a window can carry the size of the last buffer it drew, from before a resize GTK already took (Mutter sends one when native UI drawing with GL resizes from its first frames), and GTK goes back to that size. So a window drawing with GL keeps the latest bounds asked for while it is not focused, and once it is, an idle callback asks for them again if the window went back to the size it had, unless it is maximized, full screen or tiled. GTK skips a request for the size it asked for last: the callback first takes the configure and asks for the size the window has.
- A window the user cannot resize is never smaller than its default size,
which
SetBoundssets too, andSetResizable(false)to the size it has, or than its natural size, which GTK makes 200x200 when the window's child has none, as the web view: the box around the web view asks for 1x1. - On Wayland, GTK decorates a frameless window, or one with a hidden title
bar, itself (
useCSD):gtk_window_set_titlebarwith aGtkBoxthat never shows gives it client-side decorations without a title bar, so GTK draws its shadow and border around the page, resizes the window from them, and drops them while the window is maximized or in full screen. GTK sizes the window without them (gtk_window_get_size, the default size, the geometry hints), so the page keeps the size asked for, and Wayland has no positions they would shift. A window withDisableShadow, a transparent one (the decorations would outline the rectangle around a page that shows its own shape; GTK draws them on an app-paintable window too), panels and flyouts, a hidden title bar on a compositor that decorates windows itself, and every window on X11 stay undecorated (gtk_window_set_decorated): an X11 window that decorates itself has its shadow inside its X window, which the frame extents thatBoundscounts do not tell, and Mutter puts its own shadow around opaque undecorated windows there. GTK styles the decorations once, so the shadow comes with the window:SetHasShadowchanges nothing on Linux. - GTK gives windows without decorations no resize borders, so the outer
5 px of the page of an undecorated frameless window, or one with a
hidden title bar, resize it (16 px along the edges from a corner resize the corner). The web view's
motion-notify-eventshows a resize cursor there, keeping WebKit's cursor to restore, and itsbutton-press-eventcallsgtk_window_begin_resize_dragwith the press, as a Wayland compositor requires; neither event reaches WebKit. Nothing resizes a maximized or full screen window, and a tiled one only resizes at the edges the window manager allows, as with GTK's own decorations. gtk_window_realizeannounces server-side decorations to a Wayland compositor that speaksorg_kde_kwin_server_decoration(KWin, COSMIC, Sway) for every window GTK does not decorate itself, one without decorations too, and the compositor draws its title bar on it. A frameless window GTK does not decorate itself is realized before it shows and announced client-side instead (gdk_wayland_window_announce_csd, looked up at startup): GDK keeps that for theGdkWindowand requests it again whenever the window maps or the compositor answers with another mode, so the window gets no title bar unless the compositor decorates every window. Without the protocol (Mutter), or on X11, which reads the hint ofgtk_window_set_decorated, nothing decorates the window anyway.- A hidden title bar (
titlebar.go) is a window without decorations whose web view is in aGtkOverlay, under aGtkHeaderBarfor each side ofgtk-decoration-layoutthat names window buttons. Each bar shows only that side's minimize, maximize and close (decoration-layout), and CSS clears its background and, withTitleBarHeight, its minimum height. GTK makes the buttons (they need the bar inside aGtkWindow) and hides those the window cannot use, and they act as a header bar's: iconify, toggle maximized, close. The bars are measured before the web view exists, from their natural size, for the script that tells the first page, then from their allocations; a change of the layout setting rebuilds them. A Wayland compositor that decorates windows itself (gdk_wayland_display_prefers_ssd: the default mode oforg_kde_kwin_server_decoration_manager, server on KWin, Hyprland and Sway) gets no bars: such a window keeps GTK's announcement of server-side decorations, unlike a frameless one, so the compositor's title bar, or a tiling compositor's lack of one, stands for the buttons, whatever the layout says. - The app's GTK style sheet (
Theme.SetGTKCSS,style.go) is aGtkCssProviderof its own for the default screen, atGTK_STYLE_PROVIDER_PRIORITY_USER: GTK 3 looks providers up from the highest priority, and among equal ones from the last added, so it wins over the theme, MyGo's own provider (600) and the user'sgtk.css. Each sheet loads into a new provider, which replaces the one before only once it parsed (gtk_css_provider_load_from_datawith aGError, which takes GTK's first parse error instead of a warning). The core reaches it throughplatform.StyleBackend, which only Linux and the fake backend implement, so apps that never set a sheet do not link it; one set on the main goroutine beforeRunwaits inpendingGTKCSSuntilInit. WEBKIT_DISABLE_DMABUF_RENDERER=1is set before GTK starts unless the user set it, which avoids blank webviews on NVIDIA drivers, VMs and containers, at the cost of drawing pages on the CPU.App.SetDMABUFRenderer(AppOptions.DMABUFRenderer) leaves it unset: always (on), or wheredmabufSafe(dmabuf.go) finds Wayland and a GPU of Mesa's outside containers and virtual machines (auto, whose call links that probe in withLinkDMABUFProbe).- XDG desktop portal calls go through
portalCall(portal.go), which first registers the app's ID, the name of its desktop entry, withorg.freedesktop.host.portal.Registry: the portal only accepts that before any other call, and needs it for apps outside a sandbox that the desktop did not launch. Methods that may involve the user answer with aResponsesignal on a request object;portalRequestroutes it to a callback by the object's path. - On Wayland, where apps grab no keys, global shortcuts go through the
GlobalShortcutsportal (hotkey_portal.go). A session binds its shortcuts once, so every change closes the session and creates one that binds them all; the ids are canonical accelerators and the session token is named after the app, so desktops keep the keys the user picked. The portal may ask the user to confirm new shortcuts and needs the app's ID (portals from 1.21 refuse without it), so the app must be installed. KDE Plasma 5 binds the shortcuts listed inCreateSession's options, as the portal's drafts had it: portals before 1.17 pass them on, later ones drop them and Plasma 5 then binds nothing; MyGo logs the shortcuts the desktop bound no keys to. The activation token of anActivatedsignal becomes the display's startup notification id while the callback runs, so the window it shows or focuses may take the focus.
Why not GTK 4
The backend uses GTK 3 because a GTK 4 backend would do less, run on
fewer systems and give apps nothing they lack on GTK 3, at the cost of
rewriting most of internal/linux.
It would do less. Some of what MyGo's API promises has no GTK 4 counterpart, not even a renamed one:
- Window placement. GTK 4 has no
gtk_window_move,gtk_window_get_position,gtk_window_set_positionorgdk_window_get_frame_extents, on X11 too: an app cannot put a window anywhere or ask where it is.Bounds,SetBounds,Centerand the frame handling above rest on them, and flyouts are anchored to a rectangle withgdk_window_move_to_rect, of aGdkWindowGTK 4 has no more. - Window hints.
gtk_window_set_keep_above,gtk_window_set_skip_taskbar_hintandgtk_window_set_type_hintare gone: no always on top, no windows kept out of the taskbar, no utility or popup menu windows for flyouts and panels. - Tray icons. AppIndicator, which draws them, takes a
GtkMenu, which GTK 4 removed withGtkMenuBar, and has no GTK 4 version: the backend would serve StatusNotifierItem and dbusmenu over D-Bus itself.
Only X11 could get placement and hints back, through Xlib and EWMH beside
GTK (XMoveWindow, _NET_WM_STATE_ABOVE, _NET_WM_STATE_SKIP_TASKBAR),
and GTK 4.18 deprecated its X11 backend, to be removed in GTK 5: a
GTK 4 backend would rest those features on what GTK is dropping. Wayland is no
reason to move either: no GTK version places windows there.
It would run on fewer systems. GTK 4 replaced ATK, which native UI's
accessible tree implements (surface_access.go) and GTK 3 bridges to
AT-SPI, with GtkAccessible, and only GTK 4.14 lets an app expose text it
draws itself (GtkAccessibleText), which screen readers need to read
native UI's text fields and editors. Ubuntu 22.04 ships GTK 4.6 and
Debian 12 GTK 4.8, both still supported, where GTK 3 gives native UI the
same accessibility as everywhere else. GTK 4 also means WebKitGTK 6.0,
which replaces 4.1 and 4.0 rather than adding to them, so apps could no
longer fall back to the WebKitGTK 4.0 of older distributions.
It would give apps nothing. A MyGo window shows a web page or native UI, which MyGo draws itself; GTK's own widgets show only in title bar buttons, menus and dialogs. WebKitGTK 6.0 is the same WebKit as 4.1, built from the same releases, so pages render and behave the same.
And it would cost the rewrite. Beyond the above, GTK 4 replaced the
button-press-event, motion-notify-event, key-press-event and
configure-event signals with event controllers and made GdkEvent
opaque, where the backend reads its fields at fixed offsets (field[T]);
removed gtk_widget_event, which hands Tab to the web view, and
GtkLayout, which holds the web views over native UI; turned
GtkClipboard and gtk_drag_* into GdkClipboard and
GtkDragSource/GtkDropTarget; made menus GMenuModels in popovers; and
removed gtk_dialog_run and gtk_native_dialog_run, which run the main
loop until a message or file dialog is answered so that the call that
opened it returns the answer. The window manager quirks the bounds
handling above works around would have to be found again on GTK 4, across
the same X11 window managers and Wayland compositors.
GTK 3 is still maintained, installed on every desktop MyGo targets, and what WebKitGTK 4.1, AppIndicator and gtk-layer-shell are built against. The trade turns once WebKitGTK stops releasing 4.1, GTK 3 leaves the distributions apps ship to, or those that are still supported all have GTK 4.14: then a GTK 4 backend loses nothing GTK 3 still gives, and the features GTK 4 lacks become X11-only code beside it.
Windows (internal/windows)
- Win32 is called through
syscall(LazyDLLprocs; system DLLs are loaded by absolute path from the system directory).IsMainThreadcompares the thread id recorded during package initialization. The loop isGetMessageW;SignalandWakepost messages to a hidden top-level application window, which also receives tray, hot key, theme (WM_SETTINGCHANGE) and display broadcasts.QuitpostsWM_QUIT, which modal loops (dialogs, menus) forward, so quitting works while they run. - COM.
comCallcalls vtable methods; its indices come fromWebView2.h(constants inwebview2.go). Handler objects (event and completion handlers) share one vtable whose callbacks are created once and routeInvoketo a Go function per object; the objects live in a map until COM releases them. Call wrappers arego:uintptrescapes, souintptr(unsafe.Pointer(&x))arguments stay valid. - WebView2 without a loader DLL.
createEnvironmentfinds the Evergreen runtime in the registry (EdgeUpdate\ClientState\<channel>\EBWebView) and callsCreateWebViewEnvironmentWithOptionsInternalof itsEmbeddedBrowserWebView.dll, asWebView2Loader.dlldoes; a loader next to the executable wins.internal/webview2finds the runtime, for the backend and formygo doctor: a directory holding that library for this architecture and the browser processmsedgewebview2.exeat its root (the library alone is the Edge browser's, whose environments fail withERROR_NOT_SUPPORTED), and EdgeUpdate's record of a runtime outlives its files. The first window that shows a web page starts the environment (startEnvironment), and fails without a runtime: windows that show native UI need none, so an app whose windows all do runs without WebView2. The environment and each controller are created asynchronously: window methods that need the webview wait inpending. A creation that fails is tried again, three times in all, a second then two apart, as Microsoft advises unless it failed withERROR_INVALID_STATE: under load, WebView2 fails some withERROR_BUSYorCO_E_SERVER_EXEC_FAILURE. When the webview never comes, the calls that report a result (Eval, CapturePage, PrintToPDF) get the reason, as later ones do: the window closed, or WebView2 could not create it. User data lives in%LOCALAPPDATA%\<name>\WebView2. The direct path appliesWEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTSandWEBVIEW2_USER_DATA_FOLDERas the loader does; it does not read the channel variables or policy keys. - Custom schemes load from
http://<scheme>.localhost/, and their other hosts fromhttp://<host>.<scheme>.localhost/(internal/schemehost: hosts of DNS labels, lower-cased, without a port), which WebView2 lets the app answer throughWebResourceRequested. Chromium treats*.localhostas a secure origin, and unlikehttps,httpblocks no mixed content, so pages reachws://andhttp://URLs as with the custom schemes of the other backends. The backend maps URLs both ways, so the core only sees<scheme>://<host>/. See "Custom protocols" for how responses reach WebView2.LoadHTMLwith a base URL serves the document from that URL the same way. - Eval goes through the DevTools protocol (
Runtime.evaluatewithawaitPromise), which reports syntax errors and ignores the page's CSP. Edit roles rundocument.execCommandwith a user gesture, except Paste:Input.dispatchKeyEventcarries Chromium'spastecommand on anUnidentifiedraw key-down, followed by key-up once it completes. The page sees these events without modifiers, avoiding its Ctrl+V handlers, and a paste event with the clipboard's formats. The controller gets focus first, after the menu closes. Paste and Match Style still inserts the clipboard's text withInput.insertText. - Scripts injected at document creation run in every frame, so the
main-frame-only ones (the bridge) are wrapped in
window === window.top. Iframes get nochrome.webviewhandler. - ABI. On x64, structs over 8 bytes are passed by reference and Go
mirrors the first integer arguments into the XMM registers, so doubles can
be passed as bits. On ARM64, 16-byte structs travel in two registers and
syscallcannot pass floats, so zoom falls back to CSS (abi_*.go);comCallFloatcalls methods taking a double through purego instead, asput_Expiresof cookies. Go callbacks read only integer registers: the two methods of UI Automation that take doubles enter through thunks in assembly (uia_*.s), which move the doubles' bits to the integer registers of their positions, touching only registers a call may change, and jump to the callbacks. - DPI: the process is per-monitor aware (v2); the backend converts sizes
between pixels and DIPs with the window's or monitor's DPI, and points
and rectangles of the screen (windows' bounds, the displays, the cursor,
tray icons) through one space of DIPs in which displays never overlap
and a point names one display (
dipSpace, indips.go, which builds on every platform for its tests): a display's DIPs start at its top-left pixel scaled by the smallest scale of the displays, as Qt anchors them at that pixel, and go on in its own scale. The primary display's DIPs, and those of displays that all have one scale, are the screen's pixels at that scale. A window takes the scale of the display it is the most on, whose DPI Windows gives it; it is created on its display, at its rectangle, andplacemoves it, keeping its rectangle inWM_DPICHANGED, whose suggested one would scale the size, already in the new display's scale, a second time (#1039). Frameless windows, and those with a hidden title bar, drop the caption inWM_NCCALCSIZEbut keep the side and bottom borders, which Windows 10+ draws invisibly outside the window, so they still resize. On Windows 11 a hidden title bar keeps one pixel of its top, under the window's border, without which the maximize button opens no snap layouts while the window is not maximized. Windows 10, which has no snap layouts, keeps none: over a window that keeps any of its top it draws its whole title bar. A resizable frameless window on Windows 10 takes the visible pixel of its borders into its client area too, where the content covers the line DWM draws there, white whatever the content (#916), and its frame is not extended for the shadow, which its invisible borders cast: extended, it shows as a white line along the top. Other windows without a caption extend it by a pixel, without which a window with no borders has no shadow. The build comes fromRtlGetVersion, which no manifest changes. - The top edge of a window without a caption (
topedge.go), whichWM_NCCALCSIZEgives to the client area, where the webview's or the surface's window answersHTCLIENT, is a child window above them: a layered window without a bitmap (WS_EX_NOREDIRECTIONBITMAP), as tall as the sizing frame, that answersHTTOP, andHTTOPLEFTandHTTOPRIGHTnear the corners it reaches, and sends the window's presses on, so it resizes; Windows Terminal's caption works the same way. Below it,--app-region: dragstill moves the window. It spans a frameless window and stops at a hidden title bar's buttons, and hides while the window is maximized, in full screen or not resizable. - A hidden title bar (
titlebar.go) gets its controls from a child window above the webview's, beside the top edge. The buttons are a layered window (UpdateLayeredWindow, premultiplied BGRA): Segoe Fluent Icons glyphs (Segoe MDL2 Assets on Windows 10) drawn white on black with GDI give their coverage, over backplates of Chromium's alphas. It answersWM_NCHITTESTwithHTMINBUTTON,HTMAXBUTTONandHTCLOSE, passesWM_NCMOUSEMOVEtoDefWindowProc, which opens Windows 11's snap layouts over maximize, and runs the buttons itself from the non-client button messages, sending the windowWM_SYSCOMMAND. The webview's window is created later, so the controls and the top edge go back to the top of the z-order when it appears. A window without a caption has no room for its menu bar: Alt and F10 open a popup holding the bar's own submenus. - Menus are Win32's (
menu.go). Popup menus follow the appearance through uxtheme's preferred app mode (ordinal 135); the menu bar follows nothing, so the window draws it, and covers the light line below it when dark, from the UAH messages menus send their window (darkmenu.go). Windows 11 still draws Win32 menus as Windows 10 did, so there MyGo draws the items in the style of its own menus (WinUI'sMenuFlyout, File Explorer's), in either appearance (menudraw.go): popup items are owner-drawn (MFT_OWNERDRAW, measured and drawn inWM_MEASUREITEMandWM_DRAWITEMof the window the menu shows for, or of the application window for trays), each into a bitmap with its highlight's rounded corners antialiased, then copied, and excluded from the DC after, which keeps Windows from drawing the submenus' arrows over the chevrons;WM_INITMENUPOPUPgives the menu the background of the items. Windows widens every owner-drawn item past the width it asks for, by as much as it doesn't tell (notSM_CXMENUCHECK, #922): items ask for all they need until one draws, whose width less the widest asked for in its menu is that widening at the DPI, and for that much less after. Labels are measured by their advance and drawn unclipped (DT_NOCLIP), into the item's padding where their ink reaches past it (Microsoft YaHei UI, Chinese Windows' menu font, by a pixel, #1028). The items keep their strings, which mnemonics, screen readers and the tests read. A menu with owner-drawn items loses Windows 11's frame, its window square under its class's drop shadow (CS_DROPSHADOW) along two sides: while menus show (WM_ENTERMENULOOPtoWM_EXITMENULOOP, and aroundTrackPopupMenuEx, which creates its window before), aWH_CALLWNDPROChook of the main thread rounds the windows of class#32768in theirWM_CREATE(DWMWCP_ROUND, WinUI's 8 pixels), gives them one outline (DWMWA_BORDER_COLOR: a pixel of WinUI'sSurfaceStrokeColorFlyoutover the menu's background,#00000019in the light appearance and#FFFFFF19in the dark, a color of its own rather than the accent) and takes the shadow off the class, for DWM's around the window. It also subclasses them: on Windows 11 22H2 with transparency effects on, no contrast theme, energy saver off, outside remote sessions and with a GPU (glassConditions: DWM draws no material when the first DXGI adapter is Microsoft's Basic Render Driver or a software one, unless itsForceEffectModepolicy is 2), a window that says (MN_GETHMENU, before it shows or paints) that it shows a menu MyGo draws gets Acrylic (DWMSBT_TRANSIENTWINDOW, in the appearance throughDWMWA_USE_IMMERSIVE_DARK_MODE) with the frame extended over all of it, and is framed active (WM_NCACTIVATEtoDefWindowProcas active with an lParam of -1, again once shown and whatever it is told after, as Firefox's popups with Acrylic are): DWM draws a system backdrop as a flat grey in a window it frames inactive, as a menu's window, never activated, otherwise is. Its background brush turns black, which DWM shows as transparent, its border too (WM_NCPAINT, andWM_PRINTfor animations), and its items wear the veil of WinUI's Acrylic tint over the material: the fallback color at the opacity of its luminosity layer (acrylicLuminosity, 85% in either appearance), so that a menu is as light as the shell's flyouts over a dark desktop, the material showing a seventh of the way through, and the highlights and separators are WinUI's translucent fills over it, their rounded corners leaving DWM's window to show. The labels, the shortcuts and the icons go over the veil's own color, opaque, and once: what shows is the text as GDI draws it on the color the shell's menus draw theirs on, its ClearType blending with it, rather than the grayscale antialiasing of text drawn over black and over white for unblend to tell its alpha, and the pixels GDI leaves no alpha to take the item's. A window that shows another menu, as Windows' window menu, stays as it is, and so do its items. Elsewhere a menu MyGo draws has WinUI's solid background, the fallback of its Acrylic (#F9F9F9, dark#2C2C2C), its window's own border painted in it, for that one outline to be the menu's only border. Tray menus open on Windows 11 as the system's do, centered on the icon 12 DIPs from the taskbar (ABM_GETTASKBARPOS, aligned byTrackPopupMenuExand kept off the taskbar by itsrcExclude), and at the pointer on Windows 10 or for an icon not on the taskbar. The bar grows fromWM_UAHMEASUREMENUITEMand its items get inset backplates with rounded corners. A contrast theme keeps the system's colors: the bar is Windows' again, the popup items take the system's. Windows 10 keeps its menus. - Vibrancy sets
DWMWA_SYSTEMBACKDROP_TYPE(Windows 11 22H2) and extends the frame over the client area, behind a transparent webview or native UI. The material shows only in a window created without a redirection bitmap (WS_EX_NOREDIRECTIONBITMAP, which Windows neither adds nor removes later), whose opaque surface would cover it, soSetVibrancyshows one only in windows created withVibrancy. A transparent window of native UI (TransparentwithContent) is created without one too, as a flyout is, where DirectComposition is available: what its frames leave transparent shows what is behind the window. Such a window shows nothing GDI draws: it has no menu bar, like a window without a caption, and the buttons of its hidden title bar, layered without a bitmap too, show their pixels through DirectComposition (compositor.go), on a Direct3D 11 device the windows share (internal/gpu/d3d11/device, which links none of the renderer). A device found invalid after a failed draw, or whose GPU device signals its removed event (ID3D11Device4::RegisterDeviceRemovedEvent, waited for by a goroutine), is made again, and every window's buttons draw again; buttons that could not draw retry on a timer of the application window, a second later and longer after each failure in a row. Without DirectComposition, a window with a hidden title bar or native UI keeps its bitmap: what it draws shows, and the material does not. Native UI there draws on the GPU into a swap chain for composition, premultiplied by its alpha (d3d11.NewComposed), on a DirectComposition device of the renderer's own, and its frames drawn in memory show as the window controls do. A renderer about to draw takes the surface's window over from those (Surface.Native): a window has one DirectComposition target at most.ui.Context.Vibrancytells the view whether the material shows (platform.MaterialSurface). - Message boxes are task dialogs (comctl32 v6, activated from shell32's manifest for executables without one); their structs are packed and laid out by hand. Notifications are notification-area balloons, which Windows 10+ shows as toasts.
Browsers (internal/web)
With GOOS=js GOARCH=wasm, backend_js.go picks internal/web, which
shows windows of native UI (WindowOptions.Surface) on a page; windows of
web pages, menus, trays, notifications, global shortcuts and file dialogs
report errUnsupported or do nothing, as the browser has nothing to stand
for them. Building for browsers adds nothing to desktop builds: the
backend, load_js.go and canvas_js.go are js only, and the contract
gained one field, SurfaceNative.Canvas.
-
Size. What only web pages use would make most of a build for browsers, which servers send whole (Cloudflare's static files are at most 25 MiB): the root's files for pages leave it out.
ipc.go,channel.go,bridge.go(what windows say to the bridge: its script, messages,Eval),protocol.go(custom schemes, the frontend's, their downloads),typescript.go,updater.go(andrelaunchNow),single_instance.go,window_state.goandrandom.gohave twins (_js.go) that keep their API and do nothing, or report that there is no page, without the packages they need: net/http, net, crypto, encoding/json, internal/tsgen with go/parser, internal/update. OnlyProtocol.Handle,HandleFuncandFileServer, which take net/http's types, are missing there.Bindbinds nothing, so nothing callsreflect.Value.Method, which would make the linker keep every exported method of every type in an interface.internal/platformtakes anhttp.Headeras its type,map[string][]string, so that ui does not import net/http either (importing it alone adds 2.3 MB), andgpu.SourceSum, for shaders compiled ahead of time, is not built for browsers, with crypto/sha256. The root's tests andinternal/e2eare for desktops. What stays is mostly the runtime, ui and its packages, and the image codecs (ui.Image):website/docs-democomes to about 9.8 MB (2.6 MB with gzip). -
Threading. Go's main goroutine, which calls
Run, is the main thread, told by its goroutine ID asinternal/fakedoes. JavaScript calls Go's functions (js.FuncOf) on goroutines of their own, while the browser's event loop waits:Backend.synchands the event to the main goroutine throughtasksand waits for it, so that a handler can still callpreventDefault. A callback that a call from Go to JavaScript sets off (afocus()firingfocus) runs on the calling goroutine, the main one, and so runs at once.Step, for nested loops (await,PopupMenu), takes tasks asRundoes, so the browser's events go on meanwhile; a nested loop must not start in a task, whose callback holds the browser's event loop, but fromDispatch. -
Windows are elements of class
mygo-windowfilling the element with the IDmygo, or else the page, each holding a canvas and a textarea. The page sets their size;Boundsreads the element's. The surface's size is the canvas's CSS size (DIPs) anddevicePixelRatio, which aResizeObserverand the window'sresizewatch; its backing store is set before each frame to the frame's size,ceil(width × scale)as package ui computes it.RequestFrameasksrequestAnimationFrame;Occludedisdocument.hidden, where browsers give no frames, andvisibilitychangebringsSurfaceShown, orSurfaceHidden. -
Drawing. The canvas's WebGL 2 context (
gl.NewContext: premultiplied alpha, no depth, stencil or antialiasing) is current while a frame is handled, as a GtkGLArea's is in its render signal, andSurfaceNative.Canvasnames it forui/gpu_js.go. WebGL names its objects by JavaScript objects and GL by numbers:load_js.gonumbers them (the number also a property of the object, which readsFRAMEBUFFER_BINDINGback) and implements the GL functionsgl.gocalls on the current context, copying Go's bytes into one reusedUint8Arrayfor uploads, which the pixel store's unpacking (tracked fromglPixelStorei) sizes. WithWEBGL_blend_func_extendedthe shader writes the second color as on desktops; without it (one color blended) subpixel glyphs could not draw, which browsers' text never has. A lost context is kept (preventDefault) and, once restored, renumbered (Context.Renew), which makes the renderer start over. Where the browser has no WebGL 2, the canvas takes a 2D context andPresentPixelsputs the frames drawn in memory into anImageData. -
Text (
internal/text/canvas_js.go) is the browser's: a 2D context measures and draws it, so that it finds fonts, falls back and shapes as for pages. Canvases give widths, not glyphs: the engine's glyphs are grapheme clusters (go-text's segmenter), each ending where the text of its word through it ends and starting its own width before, so that the word's kerning moves the cluster it applies to; they are drawn alone, with the font at the size they were measured at and the canvas scaled (at the pixels' size the system's font may take another optical size, as SF Pro Display for Text, whose glyphs the advances would not fit), their coverage read back (getImageDataof awillReadFrequentlycontext) and cropped to the ink. A line of text draws as the browser'sfillTextdraws it whole, pixel for pixel. Fonts are shaded: a glyph is drawn in a gray of its text's shade, as Chrome's text, like AppKit's, is bolder the lighter its color (dark text drawn with white glyphs looks too bold), Thick in white; emoji take an unshaded twin of their font. Runs of scripts whose letters join or reorder (Arabic, the scripts of India, Thai, …) are one glyph each, shaped whole. Lines break at UAX #14's opportunities, and go-text's bidi package gives runs' levels, which the engine reorders (UAX #9's L2). Glyph IDs number the clusters' texts. Glyphs blend in turn (joinfalse), at quarter-pixel positions, as Skia draws them. Chrome picks the optical size of the system font inconsistently between pages and canvases, so text of native UI can be wider or narrower than a page's in the same font. -
Input. Pointer events (captured on press) are the pointer, with clicks counted from presses close in time and place, buttons 3 and 4 the back and forward keys. Those of a pointer that is not the primary one (
isPrimary), as a second finger, are left out, as no second finger presses on the desktops; a button going down or up while another is held, which comes as apointermovewith itsbutton, is aPointerDownorPointerUpas on the desktops (chord); theirpointerTypeandpressureare the device and the pressure (device).pointercancel, the browser taking the pointer, as a touch it scrolls the page with, is aPointerCancel, not a release.wheelevents scroll, lines and pages turned into DIPs, notches of 120 notPrecise. The textarea, out of sight at the caret (SetTextInput), has the keyboard:keydownandkeyupare keys, by what they type (key) and else by where they are (code);beforeinputbrings typed text, which it cancels so that the textarea stays empty; composition events are the input method's. The browser keeps its defaults for keys that type, its own keys (F5, F11, F12) and those copying and pasting: the content copies on the key and thecopyevent that follows carries its text (the backend's clipboard) to the system's, while the clipboard's text comes withpaste, which then sends the paste command. On a Mac, Command isModCtrland ControlModSuper, as package ui takes browsers for another system than macOS. -
Files dropped (
files.go):dragoveron the canvas, with files, isFileDragOver, whose answer is thedropEffect, and the browser's default (opening the file, leaving the app) is always prevented; adropreads the files' bytes (arrayBuffer, settled together; folders have none) beforeFileDropgives their paths,/mygo-drop/<n>/<name>. Go's syscall package calls thefsobject ofwasm_exec.js, whose functions fail in browsers: the backend wrapsopen,read,fstat,close,stat,lstatandreaddirto answer for those paths, synchronously, as an app reads a drop in its handler, while the browser waits on the callback that sent the drop, and passes other paths on. -
Context menus (
PopupMenu) are HTML, styled as macOS's, in a nested loop until an item is chosen, a press lands elsewhere, Escape is pressed or the page loses focus; the choice goes toMenuItemClicked. -
Themes:
IsDarkandPreferencesreadmatchMedia, whose changes callThemeChanged; the root'sdata-mygo-theme(watched by aMutationObserver) chooses the appearance over the media's.ShowMessageBoxusesalertandconfirm. -
History (
ui/router_js.go): the firstui.Routera window (not aTester) shows claims the tab's history, through hooks the router calls (browserHistory, nil elsewhere). Its location goes in the URL's fragment and the number of its entry (Router.base + at,basecounting the entries dropped pastmaxHistory) inhistory.state:PushispushState,ReplacereplaceState,Gohistory.go, whosepopstatefinds the router there already. Apopstateof the browser's own buttons goes to the entry of its number; one just before the router's first or after its last, as after a reload (which takes the number of the browser's entry for the router's), becomes the router's; a fragment typed in, without a state, is pushed.
IPC
The page runtime (packages/bridge → internal/bridge/bridge.js)
packages/bridge/src/bridge.ts is bundled by Bun into an IIFE
and committed, so building an app never needs Bun. The core wraps it with the
window's configuration (bridge.Script) and every backend injects it at
document start into the main frame. It installs:
-
window.mygo:call(method, ...args),on(event, fn),once, thewindowcontrols (minimize,toggleMaximize,close, …),platform,windowId,version. Frozen, so pages cannot tamper with it. -
window.__mygo.receive(messages), used by Go to deliver messages. -
dom-ready notification and
--app-region: draghandling for frameless windows (the mousedown is reported, the backend starts a window drag from the recorded mouse event while the button is down). -
The
--mygo-titlebar-*CSS variables of a window with a hidden title bar, in a constructed style sheet (a<style>element where engines lack them), from themygo:title-barevent: the room its controls take (Window.TitleBarof the backend, in CSS pixels). Each backend delivers the event at document start, in a script after the bridge, so the first page lays out around the controls before it paints; the core sends it again when the room changes and once the DOM of any later page is ready. -
File drops (
Window.OnFileDrop,onFileDropin mygo-runtime). Unlike Tauri, whose native drop handler takes drops away from the page (on Windows, HTML5 drag and drop needs it turned off), MyGo leaves the page's drag and drop alone and reads the paths next to it. A capture listener ondropposts{t:"drop", x, y}when the drop carries files; the core then takes the paths the backend recorded just before the page saw the drop:performDragOperation:of the web view subclass (calling super) on macOS,drag-data-receivedthendrag-drop(before WebKit's own handlers) on Linux, and on Windows the File objects the bridge posts withpostMessageWithAdditionalObjects(ICoreWebView2File.Path), with WebView2's own drop handling left on. Drags that start in the page are ignored (dragstart/dragendtracking), and so are drops the page handles; where the page does not handle dragged files, bubblingdragover/droplisteners cancel them, so a drop reaches Go instead of the engine replacing the page with the file. Paths go to Go listeners and, as themygo:file-dropevent, to trusted pages only. Event names starting withmygo:are reserved. -
The page's theme color (
theme-color.ts,Page.ThemeColor): the first<meta name="theme-color">of the head whosemediamatches and whose content is a color, which a canvas'sfillStyleparses (a color it does not serialize as sRGB is drawn and read back), posted as{t:"theme-color", v:"#rrggbbaa"}(""for none) when it changes. AMutationObserverwatches the head, not the body, so pages that change their body pay nothing, andmatchMediathe metas' media, so a change of appearance picks another. Before DOMContentLoaded it posts only colors, then whatever it found, before dom-ready: the core keeps the previous page's color from the commit until then, so pages of the same color change nothing, and drops it at the load's end for a page that never told one (no bridge, as an image). The same in every engine: WKWebView'sthemeColor(macOS 12+) would need KVO, and WebView2 and WebKitGTK have none. -
Fitting a window to its content (
fit-content.ts,FlyoutOptions.FitContent,WindowOptions.FitContent): the bridge configuration'sfit(the limits, in the CSS pixels of the page's zoom, whichmygo:fit-contentupdates as the zoom changes) adds:where(:root){width:max-content;min-width;max-width;min-height}to MyGo's style sheet, without specificity so that a page's own size forhtmlwins, with the room of a vertical scrollbar left out of the widths while the content is taller than the maximum height. At DOMContentLoaded, before dom-ready, and then from aResizeObserveron the root and the window'sresizeevents, it posts{t:"content-size", w, h, sw, sh}, the root's border box and the scrollbars' room in CSS pixels, when it changes;shonly while the content is wider than the root (the body overflows), as at the maximum width. The core turns it into DIPs at the page's zoom (a fraction of a pixel takes one) and clamps it. The vertical scrollbar's room counts only where the content is taller than the window may be (its maximum height or the work area): the page shows one for a moment whenever the content grows before the window does, and counting it then would undo a size the user just gave the window. It resizes a flyout where it goes, or a window's content bounds, only in the dimension that changed and not while maximized or in full screen, within its display's work area; a size equal to the last it took does nothing, so a user's resize holds. The same in every engine: WebKit's private-[WKWebView _setSizeToContentAutoSizeMaximumSize:]is not used. A flyout of native UI fits without the bridge: itssurface.ConngetsFitWidth(the width's limits and the work area's width, from the display the flyout is on) andContentSize, throughfitContent.native, whichNewFlyoutsets so that apps without such a flyout link none of the core's side, andui/fit.gomeasures the root as each frame lays out, right afterlayoutTree:contentWidth(root, true)within those widths, andcontentHeightat that width, the non-committing layout flex columns measure their items with, so percentages andGrowmeasure as their content, asheight: auto; absolutely placed elements and the inspector take none of it. It tells a size only when it differs from the last it told, and the core clamps it as a page's (fitNative), then resizes and places the flyout after the frame (postMain, as the backends' resizes may send the surface events at once): the surface'sSurfaceResizebrings the frame laid out at the new size. A frame laid out at a new size that measures the size told before the last one holds, so that a view building its content afterContext.Sizedoes not resize the flyout back and forth. A hidden window draws no frames, soShowandShowInactiveof a hidden flyout first callfitContent.show(fitBeforeShow), whichNewFlyoutsets withnative: where the content waits to draw a frame,surface.Conn.Fitbuilds and lays it out at once, as a capture does, and the flyout takes the size it measured before the platform shows it; the surface's next frame draws it at that size.Fitbuilds nothing within the content's frame or before its first, so the view never runs withinNewFlyout. -
Find in page (
find.ts,Page.FindInPage): the bridge walks the visible text nodes, marks matches with the CSS Custom Highlight API (a constructed style sheet, which a Content Security Policy allows) and scrolls to the active one; engines without the API get the active match selected instead. Being JavaScript, it works the same in every engine.
The transport is window.webkit.messageHandlers.mygo.postMessage on both
WebKit platforms and window.chrome.webview.postMessage on WebView2.
The mygo-runtime package (packages/runtime)
Apps reach the injected runtime through the mygo-runtime npm package:
call, on/once, typed event<T>(name), currentWindow controls,
isMyGo, isCallError, runtime() (which throws a helpful error outside a
MyGo window) and the public types (Runtime, WindowControls, Platform),
which the bridge shares. It holds no transport of its own: it delegates to
window.mygo, so the injected script stays the single implementation of the
protocol. The template depends on ^<version> from npm, released in step
with the Go module.
The mygo-cli package (packages/cli)
The CLI is also published to npm, so that projects pin it in package.json
and run it from their scripts. Like esbuild, each platform's binary is a
package of its own, @egoist/mygo-cli-<os>-<cpu> in packages/cli/npm (npm
takes unscoped names such as mygo-cli-win32-x64 for spam), with os
and cpu fields; mygo-cli lists them all as optional dependencies, so
package managers install only the matching one, and its bin/mygo.js
resolves that package and replaces itself with the binary
(process.execve in Node.js 23.11 and later and in Bun; elsewhere it spawns
the binary, waits and passes its exit status on). MYGO_CLI_BINARY points
it at another build.
Wire protocol
Page → Go (a JSON string per message, prefixed with the window's secret; see Trust):
| message | meaning |
|---|---|
{"t":"call","id":N,"k":token,"m":"Service.Method","a":[...]} |
call a bound method |
{"t":"chan-ack","c":N,"k":token,"n":S} |
the page took the values of channel N up to the S-th |
{"t":"chan-close","c":N,"k":token} |
the page closed channel N |
{"t":"dom-ready"} |
DOMContentLoaded fired |
{"t":"theme-color","v":"#rrggbbaa"} |
the page's theme color changed; "" for none |
{"t":"content-size","w":W,"h":H,"sw":SW,"sh":SH} |
the size of the content of a window that fits it, and the room of its scrollbars, in CSS pixels |
{"t":"drag"} / {"t":"dblclick"} |
mousedown / double click on a drag region |
{"t":"context-menu","x":X,"y":Y,"l":link,"lt":text,"i":image,"m":media,"s":selection,"e":true} |
the page's contextmenu event, not cancelled, at X, Y in CSS pixels: what it opens on, empty parts left out (kept only while OnContextMenu has a listener) |
Go → page, batched into one __mygo.receive([...]) evaluation per
main-loop turn. JavaScriptCore runs a script of that shape, a.b(JSON),
with its JSON parser instead of compiling it, which is several times
faster, unless the inspector is enabled (development builds):
| message | meaning |
|---|---|
{"t":"reply","id":N,"k":token,"ok":true,"v":value} |
successful call |
{"t":"reply","id":N,"k":token,"ok":false,"e":"message"} |
error or panic |
{"t":"chan","c":N,"k":token,"p":value} |
a value of channel N; "a":1 asks for an acknowledgment |
{"t":"chan","c":N,"k":token,"end":true} |
Go closed channel N |
{"t":"event","n":"name","p":payload} |
typed event |
k is a random per-page token: a reply meant for a page that has since
navigated away can never resolve a promise of the new page.
Calls
WindowHandler.Messageruns on the main thread. Messages starting with{"t":"call",are handed to a goroutine (handleCall) together with the page context and the page's trust decision, captured on the main thread so a racing navigation cannot change them. Everything else is decoded on the main thread.handleCalldecodes the envelope withencoding/json/v2, looks up the method, decodes each argument into the parameter type, calls it and encodes the result. Panics are recovered, logged with a stack trace and returned as errors. Large values are copied as little as possible: the arguments are raw values that share the message (rawValue), decoded from it in place, and the result is encoded into a part of the reply (message), which the flush copies into the script once.- The reply is queued with
Window.enqueue(msg, false)and flushed on the main thread.
Bound methods may take a context.Context first: it carries the calling
window (CallerWindow) and is canceled when the page navigates or the window
closes. Bind validates every parameter and result type up front
(tsgen.Validate), so unsupported types fail at startup, not at call time.
Bind lists a service's methods with reflect.Type.Method, and a program
that does that keeps every exported method of every type in an interface,
since the linker cannot tell which it calls: about 2.5 MB of a native UI
app. So only apps that call Bind (or Use a plugin with a service) link
it. MyGo's own service, mygo:window behind window.mygo.window, is bound
by bindFuncs, from funcs, without it; go build -ldflags=-dumpdep
prints <ReflectMethod> on what does it.
JSON options (jsonOptions in ipc.go) are shared by calls and events:
json/v2 defaults (nil slices encode as [], case-sensitive names),
time.Duration as nanoseconds, and U+2028/2029 escaped for JavaScript.
Events
NewEvent[T](name) registers a typed event. Emit and Broadcast encode
once and enqueue per window. Events are held until the page reports
dom-ready (bounded to 1024 per window) and the hold resets on every
navigation, so events sent right after creating a window, or during a
navigation, are delivered once listeners exist. Replies are never held:
module scripts may await a call at top level, and DOMContentLoaded waits for
them.
Channels
A *Channel[T] parameter (channel.go) streams values to the page that
made the call. The page's Channel goes into the call's arguments as an
id it chose; method.call creates the Go side for that page, registered
in Window.channels, and closes it when the method returns, before the
reply is queued, so the page gets the values, the end, then the result.
The page may close it earlier, which cancels the call's context, a context
derived from the page's for calls with channels; so does the page going
away. The page may even close it before the call's goroutine made it
(aborting a request right after starting it): the window remembers such
closes (closedEarly, bounded, reset with the page) and the call closes
the channel as soon as it makes it. Values are queued in the window's outbox like replies, so they are
batched and stay in order with them.
Flow control keeps a producer faster than the page from piling up
messages: every half MiB of messages, a value asks for an acknowledgment,
which the page sends once it took that value (handled it, or yielded it to
an iterator), and Send waits while more than a MiB is unacknowledged. It
never waits on the main thread, which receives the acknowledgments.
Eval
Page.Eval(code) uses the webview's native async-function API
(callAsyncJavaScript on macOS, webkit_web_view_call_async_javascript_function
on Linux), which awaits promises and ignores the page's Content Security
Policy. The code is first wrapped as return JSON.stringify({ok, v: await (code)});
if that fails to compile the code is not an expression, and it is run again as
a function body. A compile error means nothing executed, so code never runs
twice. The result travels as a JSON string and is decoded in Go. A page whose
window closed never answers, so a window closing fails the evals waiting for
it, and an eval with the quit under way fails at once.
Trust
Every page gets the runtime, but only trusted pages may call Go: the
frontend (mygo: and the mygo dev server), custom-scheme pages
(Protocol.Handle), file: and about: pages, loopback http(s) dev
servers in development, and origins listed in PageOptions.TrustedOrigins
("*" trusts everything). The decision is
recomputed on every committed navigation. Untrusted calls are rejected
without running any Go code.
PageOptions.Untrusted (Window.untrusted, fixed when the window or web
view is made) makes isTrusted false whatever the URL, for a plugin's UI
served from a custom scheme. The core then drops the page's messages above
maxUntrustedMessage before stripping the secret, rejects its calls on
the main thread instead of a goroutine each, ignores its drag regions,
leaves it out of Broadcast and of Emit to the window its web view is
in, and refuses what other pages get by default without a handler:
permissions (through isTrusted), new windows and the browser, downloads,
and main-frame navigations the page starts to another origin than its
committed page's (Window.origin). serveScheme puts the asking page in
the request's context, under the keys of CallerWindow and CallerPage,
so a scheme's handler knows which page sent a request: that's the channel
of untrusted pages to the app.
The message handler is reachable from every frame, while trust is decided
by the main frame's URL, so an iframe of another origin must not be able to
talk to Go: each window has a random secret (crypto/rand), passed only to
the bridge's closure in its configuration, which prefixes every message;
handleMessage drops messages without it. macOS additionally only accepts
messages from the main frame (WKScriptMessage.frameInfo.isMainFrame);
WebKitGTK exposes no frame information, so Linux relies on the secret.
Plugins
Use binds a plugin's service as an internal service named
plugin:<name>: calls reach it like any bound method (trust checks,
contexts, channels), but GenerateTypeScript skips it, since the plugin's
own npm package is its client. A call to a plugin that is not used fails
with an error naming mygo.Use. The official plugins in plugins/ keep
each Go package next to its npm package, built into dist/ by bun run build like mygo-runtime and released with the same version.
-
fetch streams a response through a
Channel: the head first (status, headers, final URL), then base64 chunks of the body as Go reads them. The JavaScript side builds aResponsearound a pull-basedReadableStreamover the channel's iterator, so the page's reading paces Go through the channel's flow control. Aborting, canceling the body or the page going away closes the channel, which cancels the request's context. Request headers go through a plainHeaders, which, unlike aRequest's, drops no forbidden names. -
websocket is an RFC 6455 client of its own (no dependency) on top of
net/http, which keeps upgrade requests on HTTP/1.1 and hands the connection over as the body of the 101 response, so proxies and the client's TLS settings apply.Connectstreams the connection's events through a channel for as long as it lasts; the page sends withSendcalls numbered in order, since calls run on goroutines of their own and would otherwise race, and Go writes them in that order. Connections are keyed by window and a random id the page chooses. -
updater is the update window, in the manner of Sparkle, built on
mygo.Updateralone. A session is a check and what follows it (the release notes, the download, the offer to relaunch): a goroutine that sets the session's view (title, message, progress, release notes, buttons) and waits for responses, whether or not the window shows, so that a background check shows it only when it has something to offer and a "Check for Updates…" during one just shows it. The window's page, one embedded HTML file loaded withLoadHTML(anabout:blankpage, so trusted), watches the views through aChannel(Watch) and answers withRespond; each set of buttons has a prompt number and only the first answer to the current prompt counts, so a double click cannot answer the next view. The plugin's service rejects calls from other windows. Release notes are Markdown parsed in Go (internal/markdown), which keeps all HTML as text and only links http(s) and mailto URLs; the page gets them rendered as escaped HTML, and a Content Security Policy with a nonce runs only the page's own script: the page may call Go, and the notes come from the unsigned manifest. Links open in the browser (OnWillNavigate). Views with release notes have a fixed size; status views ask for the height of their text (Fit). The window gets an empty menu of its own, so it has no menu bar on Linux and Windows. A window sees its session throughinternal/frontend: the session creates it (NewWindow: its size, its menu, the session canceled when it closes), and the window shows the views (Current), answers (Respond) and asks for a size (Fit).plugins/updater/nativeis the same window in native UI:native.Newbuilds the plugin with a window of its own through a hook package updater sets (frontend.Plugin), so package updater links no native UI and has no API for other windows. Its view rebuilds when the session's view changes, measures the boxes of a layout in the next frame (Bounds) to ask for the size the page's script would, and builds the notes' paragraphs of inline text elements, their links ofui.Link.updater.jsoninPathUserDatakeeps the preferences, the skipped version and the time of the last check, and a change that alters them calls the functions ofOnChangeon the main thread, from a goroutine of its own so that no caller waits for the main thread; the next check is due an interval after it, or an hour after a failure, and is rescheduled on resume since timers stop while the computer sleeps. An update installed while the app runs is remembered, so that checks offer to relaunch instead of installing it again. The texts areStringsin the language that best matchesOptions.LanguageorApp.Locale(matchLanguage: language, script, region; Chinese scripts inferred from regions such as TW; another variant of the language before English), among the plugin's translations (translations.go) and the app's, whose empty fields fall back to the plugin's, then English. The page getslang, which picks CJK fonts, anddir; status texts useunicode-bidi: plaintextand the notesdir="auto", as either may be in another language than the window. The page reports the width its buttons need too, as translations can be long. -
sqlite runs SQLite in a native module,
internal/sqlitecore, whose Zig compiles SQLite's C amalgamation, whichzig/build.zig.zonpins by its hash, with SQLite's symbols hidden in the library (WebKit loads the system SQLite globally on Linux). The module'sEngineopens aConn, which runs one statement a call, prepared with the rest of the SQL compiled first so that extra SQL never runs, and returns its rows in one reply. An authorizer denies transaction control, ATTACH and VACUUM INTO outside the calls that allow it. Package sqlite's connections serialize operations and validate values in Go; canceling an operation, or closing the connection, callsConn.Interrupt(sqlite3_interrupt, which the busy handler checks too) and waits for it before the next one. Transactions hold one connection through BEGIN IMMEDIATE, COMMIT or rollback. The page client preserves int64 and BLOB values with tagged cells and confines database files to the Go-configured directory. Connections belong to their page and close on navigation or app quit. Go and native UI apps can also open connections directly. -
watch gives pages
mygo.WatchFileswatches of the directories and files the app names, by their names (Roots, orResolvefor those granted while it runs).Watchstreams a part per batch through a channel for as long as the watch lasts, and returns how it ended, a code the page turns into aWatchError. A page's watches count against its quota bymygo.CallerPagewhile their call's context is not done, as navigating ends the page context before the watches have stopped; they stop with it. -
terminal is a terminal for native UI: a
Terminalruns a program in a pseudo-terminal and emulates it with libghostty-vt, Ghostty's terminal emulator, andViewdraws it with package ui and takes its input.- The library is loaded with purego (
internal/vt):dlopen, orLoadLibrary, then a symbol table; functions taking structs or floats by value go throughpurego.RegisterFunc, the rest throughSyscallN. The Go mirrors of the C structs are checked against the layouts the library describes in JSON (ghostty_type_json) as it loads, as are the enum values the package hardcodes, and packed cells (GhosttyCell) are read by the bit positions it reports, so that a library of another version fails to load. Its callbacks (writing to the pty, the title, the bell, the clipboard, synchronized output, device attributes, …) are made once and find their terminal by the userdata. - Pseudo-terminals (
internal/pty) are opened in pure Go:/dev/ptmxwith the ioctls ofgrantpt,unlockptandptsname(TIOCPTYGNAMEon macOS,TIOCGPTNon Linux), the program in a session of its own with the slave as its controlling terminal; ConPTY on Windows (CreatePseudoConsole, a process attribute list), whose console the terminal closes once the program exits. The user's shell starts as a login shell (-zsh), when the first frame of a view sizes the screen (or after half a second without one), so that it starts at its size. A key the view takes tells Windows' backend to open no menu for theWM_SYSCHARof Alt and the key. - Threads. A goroutine reads the program's output and writes it to the
emulator under the terminal's lock, then asks the view for a frame;
another writes what is typed, from a queue, so that a program that does
not read cannot block the reader, which answers its queries. Callbacks
of the app run after the lock is released. A frame takes the lock only
to copy the emulator's state into a render state (
render_state's first phase); a second lock guards the render state, which a program's synchronized update (mode 2026) holds: the render hold callback copies the screen as the update starts, and frames show that copy until it ends, or for a second at most. Locks are taken in that order. - Drawing. The view sizes the grid in device pixels from the font
(cells as wide as the widest ASCII glyph, as tall as the line) and
rebuilds only the rows the render state marks dirty: runs of cells of
one font and color, shaped with
ui.Shape(through an LRU of shaped text, as output repeats) and placed in their cells, the glyphs of a cell centered there and shrunk when too wide, as emoji are; backgrounds and lines merge across cells. Box drawing, blocks and Powerline's separators are drawn, as in Ghostty, so that lines join across cells. Frames paint every row from these caches, the cursor (its cell's text again in the cursor's text color under a block), the selection (its text clipped to its cells and drawn again in the selected text's color when the theme has one), an input method's composition and a scroll bar. - Themes.
ghostty_themes.goholds the themes the Ghostty it binds ships (iTerm2-Color-Schemes' archive that itsbuild.zig.zonnames), as 22 colors each in the generated theme data.GhosttyThemelooks for a theme file of the user's first, in Ghostty's themes directory, as Ghostty does. - Input comes as it happens (
ui.Element.HandleInput): keys are encoded by libghostty-vt's key encoder, set from the terminal's modes (legacy, modifyOtherKeys, the Kitty keyboard protocol). A key that types text waits for the text, which macOS and Windows send after it, to encode both; text that comes alone, as on Linux or from input methods, finds its key from US layout. Mouse reports go through the mouse encoder while the program asks for them; otherwise a selection gesture of libghostty-vt turns presses and drags into selections, with the clicks it counts itself. Keys meet the terminal's keybinds before the encoder: Ghostty's defaults for the platform with the app's over them, as Ghostty layers its config. A paste reads the clipboard on the main thread as it starts (every format forOnPasteor a program in mode 5522, else the text), and the terminal keeps a snapshot: the program's read (OSC 5522) is answered duringWrite, on the reader's goroutine under the terminal's lock, where waiting for the main thread could deadlock. The binding gathers the library's reply and hands it over in one piece, which the input queue writes in pieces that report their progress. Any other read of the clipboard (ClipboardRead: OSC 52's "?" and OSC 5522, unless a paste granted it) is queued instead: the binding holds the library's own refusal back, and the view, on the main thread, reads the clipboard and writes the answer, which the reader's lock makes safe. - Shipping the library.
native/is a Zig package whosebuild.zig.zonpins Ghostty at a commit and whosebuild.ziginstalls itsghostty-vtartifact (emit-lib-vt, stripped), built like any native library withbuild(needing macOS 13,"macOS": "13.0").mygo-plugin.jsonnames its build for each platform, published as release assets with their SHA-256 and source hashes. The CLI puts them into apps (see the CLI's resources); other programs download theirs into the user's cache once, which packaged apps never do.internal/themeswritesghostty_themes.gofrom the themes archive the pinned Ghostty names.
- The library is loaded with purego (
-
game has what games need beyond what apps do, opt-in, out of core
ui, on its public API only.Viewis a focusable box that keeps its loop inui.Local: its input handler gathers keys (byPhysicalKey), buttons and wheel into theInputof the next step, and its draw function asks for the next frame (AnimationFrame), adds the time since the last frame (Painter.Now, at most a quarter of a second) and runs the steps it covers beforeDraw.Camerais aui.Transform, snapped to device pixels when unturned. Its packagegamepad, likeaudio, stands apart from windows and the main thread. One mutex guards the pads.Padsasks the platform'sscanfor the devices connected at most once a second, handing it the devices it has so that it reuses them, and a device that is gone, from a scan or when read, disconnects its pad.Statereads a device on the calling goroutine into the standard layout (State.Buttonsin the W3C's order,Axeswith y down):- macOS: GameController.framework through purego's objc:
GCControllercontrollerswith anextendedGamepad, each element'svalueread inside an autorelease pool, afterrespondsToSelector:for the buttons older systems lack. The framework finds controllers on the main run loop. - Linux: evdev, opened non-blocking where
EVIOCGBITshowsBTN_SOUTH; reading drains the events waiting into key and axis arrays, with axes scaled byEVIOCGABS's range. Xbox controllers (Microsoft's and Valve's vendor IDs, or thexpaddriver) report X and Y under BTN_NORTH's and BTN_WEST's codes, which are swapped back. - Windows: XInput's four slots, through
XInputGetStateEx(ordinal 100) for the Guide button where it exists. Empty slots, slow to ask, are asked only by scans. - Browsers:
navigator.getGamepads()by index, the standard mapping matching the package's order. Tests replacescanDeviceswith fake devices; on Linux,TestEvdev(MYGO_UINPUT=1) makes uinput pads in the test container, and on macOSTestGameControllerreads a controller fromcontrollerWithExtendedGamepad.
- macOS: GameController.framework through purego's objc:
-
glass is Liquid Glass for native UI, as macOS 26 and later draw it:
glass.Glass, aui.Material(interactive glass builds with its element, following its press withAnimate, and paints itself grown while pressed, as macOS 27's: by a fixed 1.1 DIPs left and right and 0.45 above and below, measured fromNSGlassEffectView), paints an effect (glass.Effect, see Effects under Native UI), whose parameters are a pane's material in device pixels. Its shader (glass.metal,glass.hlsl,glass.glsl, andpixels.gofor the CPU) takes what is behind the pane, blurred, sampled where the curved surface refracts it: displaced toward the middle within the bezel by Snell's law through Apple's squircle profile (lens, alongnormal), which mirrors what is just inside the rim; then maps its lightness and colors (tone; the CPU keeps its curve in a table, as its power is slow there), tints it, and lights the rim by how its normal faces the light. The defaults follow macOS 27's, measured fromNSGlassEffectViewover test patterns; the optics follow the open-source reproductions of Liquid Glass.glass.Blur, a backdrop blur, is a second effect (blurEffect): what is behind the element, blurred, with its alpha (the effect samples the backdrop's texels itself, as the heads'sampleBackdropgives no alpha), so over nothing it stays transparent. Masked by aLinearGradient, the blur varies as the gradient's alpha (blurLevels): the element paints levels in turn, each blurring what the levels before painted (blurs add as their variances do), shown where the blur wanted is above its band, by how far into the band, so a level's top doubles from at most 2 pixels to the blur's most and a pixel between two levels mixes their blurs. A level of a square element covers only where it shows (blurWanted), and so reads less. Bounding the levels after the first to the element, so that they read no unblurred content around it, measured worse against a blur varying per pixel than leaving them reading it: the repeated edge biases more than what is around. A level may tone what it shows (blurTone: saturation, an offset, an opaque color over it), which the hard style ofglass.ScrollEdge, macOS's scroll edge effect, frosts with in one level; the soft style is a gradient of the background, as macOS 27's replays its window's background under a mask, with no blur (measured from SwiftUI'ssafeAreaBarover test patterns, its layers dumped). Both effects' shaders are compiled ahead of time on macOS (shaders_darwin.go) and on Windows (shaders_windows.go), withinternal/gen.glass.Grouppaints one surface over a row of elements: a shader of packageui/shader(Kage, compiled for every renderer and the CPU) takes up to 8 rectangles and radii, unions them with a smooth minimum (so panes that nearly touch bridge), and draws the glass once around the union's bezel.Glass{Native: true}asks the window's surface for the system's glass:ui.NativeGlassMaterial, noted when the frame commits (ui/nativeglass.go), tells the surface where the glass is (platform.GlassSurface.PlaceGlass). On macOS the surface puts anNSGlassEffectViewunder the content and above the web views, and the material leaves a hole where it shows, so the glass samples a web view's page or the window's material, never the frame's own pixels; elsewhere, and with Reduce Transparency, the shader draws it. The materials readui.Painter.Preferences: Reduce Transparency draws opaque surfaces, Increase Contrast lights the rim. Window-level glass takes a tint throughNSGlassEffectView'stintColor. -
loading is loading.dev's 29 indicators (
loading-dev, React and CSS) for native UI, on ui's public API only. Each is a box ofRoleProgresswhose draw function asks for the next frame (AnimationFrame) and computes its pose from how many cycles it has run: since it first showed (its start inui.Local, as a CSS animation starts with its element), or held whilePaused. Parts are staggered as loading-dev's negativeanimation-delays, keyframes eased stretch by stretch with CSS'scubic-bezier(frames,bezier), and shapes built asui.Paths in the SVG's viewBox through an affine transform (affine,shape), arcs as cubic Béziers, which an affine transform keeps exact: Atom's rings turning in 3D are ellipses, Flip's square is projected in perspective point by point. Round caps areStrokePath's; flat ones fill the stroke's outline. Gradients are conic ones (Comet, Orbit), and Radar's radial fade is sectors of growing radii over each other, each as opaque as makes its band the gradient's opacity, so that no two edges meet inside the beam. WithReduceMotionan indicator draws loading-dev's pose for reduced motion and asks for no frames. The indicators were checked against loading-dev rendered by a browser at the same points of their cycles, with every option.
Typed client generation (internal/tsgen)
mygo generate builds the app and runs it with MYGO_GENERATE=<file>;
App.Run then writes the client (WriteTypeScript) and returns before
touching the GUI. It reaches the generator through what Bind, BindAs
and NewEvent register (ipc.writeClient): an app that binds nothing
writes no client, and links neither the generator nor go/parser (375 KB).
The generator combines:
- Reflection for correctness: types follow json/v2 encoding (embedded
structs are inlined with v2's conflict rules,
omitempty/omitzerobecome optional fields, pointers becomeT | null,[]byteis a base64 string,time.Timea string, types with JSON methodsunknown, text marshalersstring, generic instantiations get names likePageTask). - Source code for readability, when available: the entry PC of each bound
method (
runtime.FuncForPC) locates its file, which is parsed withgo/parserto recover parameter names and doc comments (JSDoc). Named string/number types with constants become union types ("all" | "done"), including simpleiotasequences. Packages not reached through a PC are located withgo list. Without sources (e.g.-trimpathbinaries) the client is still correct, just witharg0names and no docs.
The output imports call and event from mygo-runtime and declares the
interfaces, one object per service with camelCased methods, and an events
object. WriteTypeScript only rewrites the file when its
content changes, so dev servers don't reload needlessly.
The language the app shows (locale.go)
The app shows one language, which localeState.shown holds with the
user's languages and regional formats: an atomic pointer the main thread
replaces (updateLocale), so that App.Locale, the menus, the catalogs'
i18n.Locale and native UI read it from any goroutine for nothing. The
first read works it out on the main thread. updateLocale reads the
backend's PreferredLanguages, Region and, once, the saved language
(SavedLocale, where the system keeps one, else the file locale in
PathUserData; MYGO_LOCALE over both), resolves it with i18n.Match
against App.Languages(), and, when it changed, tells: windows of native
UI (Conn.LocaleChanged, after which ui reads Conn.Locale and
Conn.Region and sets the text system's language), pages (the event, and
Page.SetUserScripts with a bridge configuration of the new language, for
pages loaded later), menus (relabelMenus: items whose label is still
their role's take internal/l10n's), the backend (SetLanguages: the app's
language, then the user's others) and the listeners. SetLocale calls the
main thread may not have reached yet show the last tag only (a
generation counter). ChosenLocale reads the choice (override, after
loadLocale) on the main thread, behind the SetLocale calls queued
before it. A backend's LocaleChanged reports changes of the
system's languages or formats, which updateLocale compares.
The text engine takes a language per style and span (text.Style.Lang),
and one of the system for those without (System.SetLanguage, a new
generation of layouts): Core Text's kCTLanguageAttributeName,
DirectWrite's locale of text formats and of ranges
(IDWriteTextLayout::SetLocaleName), Pango's language attributes. Han
characters in a language that does not write them get the engine's own
choice (langRuns), as Chromium's LocaleForHan does; Core Text does so
by itself. DirectWrite's choice goes by the locale of the user's regional
formats (GetUserDefaultLocaleName), whose fallback for English mixes Yu
Gothic UI and Microsoft JhengHei UI in one Chinese text: where that locale
writes no Han characters, they take the first of the user's preferred
languages that does (userHanRuns, userHanLanguage), read once, the
first time a text has Han characters in no language that writes them
(dwrite.userHan), so that apps without them read nothing: the display
languages (GetUserPreferredUILanguages), else the languages of the
user's profile, which list languages without their language pack too, as
Chinese below English on English Windows (resolveUserHanLanguage). The
profile's are read from the registry (HKCU\Control Panel\International\User Profile, value Languages), where Windows keeps what
GlobalizationPreferences.Languages returns, in microseconds and on any
thread, rather than through the Windows Runtime.
WebView2's environment takes the language through
ICoreWebView2EnvironmentOptions, a COM object of ours
(webview2_options.go) whose vtable's callbacks are made once; a runtime
that fails with it is asked for an environment without it.
Catalogs of messages (i18n, internal/i18ngen, internal/l10n)
Package i18n is a leaf (it imports golang.org/x/text/language only):
- CLDR data is generated, never written by hand:
go generate ./i18nrunsi18n/internal/gen, which reads the npm tarball of cldr-core at the version it pins, checked against its SHA-256, and writestables.go. Plural rules are compiled to Go, aswitchper kind (cardinal, ordinal) over the distinct rule sets with a sorted table of languages, so nothing is built at init; an operand of more than 18 digits keeps its last 18 plus 10^18, which keeps its remainders by powers of ten and equals no constant of the rules. Rules are looked up by truncation, as ICU does (CLDR's parent locales of plurals are empty); messages fall back alongParent, CLDR's main parent locales with its nonlikelyScript rule. The tests check every sample CLDR gives and ICU's categories for 200 numbers in every language (scripts/plurals-golden.ts, run with Node, whose ICU is upstream; Bun on macOS uses Apple's, which differs). - Formatting data of CLDR's modern locales (effective coverage
"modern", 430 locales) comes from cldr-dates-full, cldr-numbers-full and
cldr-localenames-full, pinned like cldr-core (
format.goof the generator writesformat_tables.go). A locale's data is a record of slots (month and weekday names by context and width, day periods, eras, date, time and date-time patterns, the availableFormats of the skeletons MyGo formats, time zones' GMT formats, number symbols, patterns and digits of the default numbering system and of latn); each locale stores only the slots in which it differs from its nearest ancestor of the list (or from a synthetic root of the values most locales have), as indexes into a pool of strings. The slots come in two halves, each with its own entries and pool, packed with DEFLATE in a constant of its own: the numbers with the locales and their parents (fmtPackedNumbers, 3 KB), which every app links, as MyGo's own strings format numbers, and the dates with the names of languages (fmtPackedDates, 77 KB), which only apps that format dates or name languages link. At run timecldr.goinflates each once and puts a locale's record together on its first use (an atomic pointer per locale), its dates on the first date it formats (withDates);keyOfcaches what a tag formats with (its data locale, byParentand likely scripts, and its-u-hc,-u-fw,-u-nu). Week data, time data and day period rules are small tables by region.dtpg.goports ICU's DateTimePatternGenerator (dtTypes, distances, the pattern map keyed by base skeleton and types in ICU's order, adjustFieldTypes, date and time put together with the atTime glue), anddate.goV8's handling of styles and hour cycles;number.gowrites decimal strings, so that rounding is decimal as ICU's. Formatters are kept per locale and options in maps under a read lock.scripts/format-golden.tsrecords Node'sIntl(upstream ICU) for every locale;TestFormatMatchesICUcompares, andknownDifferencelists, with reasons, the cases that differ on purpose. - Relative times (
relative.go) use dateFields from the same pinned cldr-dates-full tarball, written torelative_tables.goin a compressed blob of their own: runs of slots asrecordSectionswrites them, by style, unit, and words for offsets -2 to 2 or past/future plural form. OnlyNewRelativeTimeFormatterreaches it, so formatting dates or numbers links none of it. The formatter uses the number formatter (half-even, as ICU's RelativeDateTimeFormatter does), the plural form of its rounded number, and ICU's rounding of offsets to hundredths when choosing words withAuto.scripts/relative-time-golden.tsrecords Node's Intl for every formatting locale, with the full matrix compressed intestdata/relative_time_icu.json.gz; the tests name the CLDR/ICU data differences explicitly. - MessageFormat (
message.go,format.go) parses ICU's syntax as ICU's MessagePattern does (apostrophe mode DOUBLE_OPTIONAL,#only in a plural's own cases), and formats numbers and dates with the formatting above; a plural case is picked on the number as#writes it. Dates go throughtimeFormat, whichNewCatalog,NewLibraryCatalogandParseMessageset (linkTimes): MyGo's own catalog, which every app formats and which has no dates (its test checks), is made throughinternal/i18nlink, which does not, so an app that formats no date links no date formatting.mygo-runtime/i18nis its TypeScript twin overIntl(skeletons turned intoIntl's options, as V8 turns them back);i18n/testdata/messages.jsonholds the cases both test. - Catalogs read a language's file once, on its first message
(
sync.Once), and cache the tags a locale looks keys up in. A library's catalog (NewLibraryCatalog) looks a key up in the registered catalogs of the app under<namespace>.<key>before its own, at each tag of the chain.Registerdeclares the languages, which package mygo reads withLanguages;SetLocaleFuncis how package mygo tells the language the app shows toLocale, which the generated functions read.
A catalog keeps each message formatted without arguments, by locale and
key, as of the registry's version: the controls of native UI say theirs
every frame (l10n.In(c.Locale()).Undo()), for a map lookup under a
read lock and no allocation. ui's context.formats works out, once per
change of the language or the system's formats, the region the controls
follow (CLDR's for the language when the system says none) and its
Region.FormatLocale; context.formatted keeps the dates and numbers the
calendar, the date and time fields and the color picker write, so a frame
formats nothing it formatted before.
Backends say a few words of their own (the buttons of pages' alert,
confirm and prompt panels, file dialogs' titles, the about panel's
title): package mygo gives them in the app's language through
platform.SetTexts as the language changes, so that internal/<os>
does not import internal/l10n.
internal/i18ngen checks catalogs and writes their functions:
catalog_gen.go in the catalogs' directory (an embed, the catalog,
registered for apps, and a function and a method of Messages per key)
and the TypeScript module, which carries the messages with each
language's fallback chain. internal/l10n is MyGo's own catalog,
generated by the same code (go generate ./internal/l10n, with
i18ngen.Options.Own), whose keys apps change as mygo.<key>; the CLI
checks those keys against it (l10n.FS, the files as written, which only
the CLI links). It embeds English only, as messages/en.json, which the
generator writes from en.json without the descriptions (7 KB of 28): each translation is a package,
locales/<tag>, that adds its file to the catalog as it is imported
(i18n.Catalog.Add), so that an app links only the languages it ships.
The generator writes those packages and l10n.Languages; the CLI passes
that list to internal/i18ngen, whose Go code imports the translations
that suit the app's languages, and package mygo tells an app that ships
one of them without the translation, as it starts. The
CLI (cmd/mygo/i18n.go) writes the functions before anything compiles,
in generate, build and every build of dev, where the catalogs'
directory is watched and the generated Go file is not, so writing it does
not trigger another build.
Custom protocols
Protocol.Handle(scheme, http.Handler) lets pages load <scheme>://localhost/…
like a web origin (fetch, ES modules, relative URLs). Protocol.serve runs the
handler on a goroutine with a schemeWriter: headers and body are buffered
and handed to the main thread in 256 KiB chunks (and on Flush), the content
type is sniffed when missing, panics become 500 responses. Backends that
implement platform.SchemeBodyWriter take the body on the handler's
goroutine instead, where they may block it, and the chunk buffer is reused.
Backends turn responses into native ones:
- macOS:
WKURLSchemeHandler;didReceiveResponse:/didReceiveData:/didFinish. A task stopped by WebKit cancels the request context and later writes are ignored (touching a stopped task raises an Objective-C exception). - Linux: WebKitGTK wants a
GInputStream, so the response body is streamed through a pipe (g_unix_input_stream_new), which WebKit reads on the main loop, 8 KiB at a time. The handler's goroutine writes the body into the pipe (WriteBody), waiting while it is full, so the main loop never blocks and a response WebKit reads slowly does not pile up in memory. Custom schemes are registered as secure and CORS-enabled. - Windows: the handler's goroutine writes the body into a pipe
(
internal/schemestream,WriteBody) that holds at most 1 MiB not read. A body that ends within it is answered whole atFinish(SHCreateMemStream), as WebView2's documentation asks of a response whose deferral completes. A larger one is answered as the pipe fills, with anIStreamof Go's (bodyStream,scheme_stream.go) whoseReadwaits for the handler: WebView2 reads response streams on a background thread of its own (one for all of them, as reported in WebView2Feedback#3519), and the stream is agile (IAgileObject, and the free-threaded marshaler aggregated forIMarshal) so that it is called there, not through a proxy on the UI thread. AReadthat comes on the UI thread anyway waits at most a second, then breaks the response off. The finalReleasecancels the request's context; the window's closing cancels those of handlers still running. WebView2 seems to give a response to the page only once it read all of it, and asks open ranges for media, so the backend narrowsRange: bytes=X-of media requests to 4 MiB (schemestream.ClampRange): the element asks for what follows (#1502).
Schemes are registered per webview at creation time, so call
Protocol.Handle before creating windows. FileServer(fsys) serves an
fs.FS with an index.html fallback for client-side routing.
The frontend (frontend.go)
Apps load their web UI with URLs without a scheme (WindowOptions.URL: "/",
LoadURL("/settings")), which resolveURL resolves against the frontend,
as in Tauri:
- during
mygo dev, the dev server:MYGO_DEV_URL, fromdevUrlof the configuration (only honored whenIsDev); - otherwise
mygo://localhost/. Themygoscheme is registered with every webview and, unless the app handles it withProtocol.Handle, serves the files given toSetFrontend.mygo buildcallsSetFrontendfrom a generated file that embedsfrontendDist(see CLI), so app code has no//go:embedand development builds need no built frontend. Duringmygo devwithout a dev server it servesMYGO_FRONTEND_DISTfrom disk, and with nothing to serve, a page explaining how to get a frontend.
Windows, lifecycle and quitting
NewWindowvalidates options, waits for readiness when called off the main thread, buildsplatform.WindowOptions(all defaults applied, bridge and preload scripts, registered schemes) and registers the window.- Called on the main thread,
NewWindowleaves showing a window of native UI to the main loop's next turn (showLater,settleShow), so that its view first runs onceNewWindowreturned and can use the window kept from it (#1053): shown withinNewWindow, a window draws its first frame at once on some systems, as AppKit may display a window it orders in, and Windows a window maximized as it shows (WM_SIZE). The frame the content asked for draws on that turn too, while the window is still hidden, as a window madeHiddendraws its first (drawSoon,drawHidden), or as it shows. A method acting on the native window (do,get) shows it first, so that it finds the window asNewWindowreturned it, but not from within a frame (framing), whose view may call one (Title): a frame drawn as the window shows would run inside it.Show,ShowInactiveandHidetake the deferred show's place, soHideright afterNewWindowkeeps the window from showing. Called from another goroutine, whichNewWindowreturns to while the main thread goes on,NewWindowshows the window before it returns, as it does a window of a page, whose page loads later anyway. - A window's page API is on
Page, a handle on the window thatWindow.Pagereturns unless the window shows native UI; the page's state stays inWindow, where IPC and the backend's callbacks reach it. - The user's close (
WindowHandler.ShouldClose) andWindow.Closeboth emitOnClose, which can be prevented.Destroyskips it. The backend reportsClosedsynchronously; the core unregisters the window, closes child windows, cancels the page context and, when it was the last window and no quit is in progress, runsOnWindowAllClosedlisteners or quits. - The quit sequence (
Application.prepareQuit) is:OnBeforeQuit→ close every window (anyOnClosecan cancel) →OnWillQuit→ stop the loop →App.Runreturns →OnQuit. It is used forApp.Quit, Cmd+Q, quit Apple Events, and SIGINT/SIGTERM alike (signal_unix.go; a second signal exits immediately). When the OS ends the session instead (AppHandler.Terminating:WM_ENDSESSIONon Windows,applicationWillTerminate:on macOS), the loop does not stop first:Application.endSessioncalls the same listeners, withQuitEvent.SessionEndingset and what they prevent ignored, thenfinish; it skips them when a quit's sequence ran or is running (quitting,stopping). Linux has no such path: the session's end arrives as SIGTERM. - Taskbar state on Windows (
taskbar.go) lives on the window and is applied when Explorer sendsTaskbarButtonCreated(first show, Explorer restarts), so progress and a hidden button set before showing stick. WindowOptions.StateKey(window_state.go) remembers a window's normal bounds and maximized/full screen state inwindow-state.jsoninPathUserData, like Tauri's window-state plugin, or in the app'sWindowStateStore(App.SetWindowStateStore), which loads all the states once and saves them all, on the main thread. The state is captured 300 ms after the last move/resize/state event (transitions resize the window on the way, and the normal bounds only change in the normal state) and when the window closes; it is saved when a window closes and when the app quits, if it changed, and kept for the next save when saving fails.App.ClearWindowStatessaves an empty map and setswindowStates.cleared, after which windows capture nothing for the rest of the run, so the quit's saves (as windows close, and infinishbeforeOnQuit) cannot bring the states back. A saved window that would not show on any display keeps its size and is centered. On Windows, where a window's position in DIPs changes with the displays' scales (dipSpace), and earlier versions' could not tell displays of different scales apart, the state also keeps the window's rectangle in pixels (platform.WindowStateWindow), and the backend creates the restored window at it, on its display (platform.WindowStateBackend,WindowOptions.StateBounds); a state without pixels is restored in DIPs where every display has the same scale, and centered elsewhere.WindowOptions.Maximizedstarts a window maximized:zoom:on macOS,gtk_window_maximizebefore mapping on Linux,SW_SHOWMAXIMIZEDon the first show on Windows, whereMaximizeon a hidden window also waits for it to be shown.- Deep links (
deeplink.go).mygo buildandmygo devlink the name, version, identifier andurlSchemesof the configuration into the binary (-X …packageName=…), which is how Linux builds know them (IsPackaged,Name,Version) and every platform knows which launch arguments are deep links. URLs of those schemes, and of ones registered withRegisterURLScheme, reachOnOpenURL: from Apple Events on macOS, from the launch arguments once the app is ready, and from the arguments a second instance forwards.RegisterURLSchemewritesHKCU\Software\Classes\<scheme>on Windows and, on Linux, a hidden<id>.url-handler.desktopin$XDG_DATA_HOME/applicationsmade the default in$XDG_CONFIG_HOME/mimeapps.list(whatxdg-mime defaultdoes); on macOS it callsLSSetDefaultHandlerForURLSchemefor a scheme the Info.plist declares. The Linux.desktopofmygo buildalso declares the schemes (Exec=… %u,MimeType=x-scheme-handler/…). - Starting at login (
login.go):SetOpenAtLoginregisters the bundle withSMAppService.mainAppServiceon macOS 13+ (a launch agent runningopen -aon macOS 12; both need a bundle), writes an XDG autostart entry on Linux, and a value ofHKCU\…\CurrentVersion\Runon Windows, whereOpenAtLoginalso honors Task Manager'sStartupApproved(odd first byte: disabled). The Linux, Windows and launch agent commands pass--mygo-opened-at-login, which the package removes fromos.Argsat init andWasOpenedAtLoginreports; macOS login items are recognized bykeyAELaunchedAsLogInItemin the launch Apple Event instead. Relaunchruns the quit sequence, then (afterOnQuit, which releases the single instance lock) starts the executable again with the same arguments, or those passed toRelaunchin their place (relaunchArguments), and the working directory it started in: on macOS throughopen -n <bundle> --args …, or, with an update staged, through the update's helper, which readsopen's arguments from therelaunchfile of its work directory (aset --line of the shell, quoted), written only then. A development build exits with code 75 instead, after writingRelaunch's arguments, each ended with a NUL byte, in the file thatMYGO_DEV_RELAUNCHnames (.mygo/dev/relaunch-args), andmygo devstarts the same build again with them, also when that happens before it was ready.- Updates (
updater.go,internal/update,cmd/mygo/updates.go), in pure Go on every platform.mygo keygencreates an Ed25519 key pair; withupdatesin the configuration (the public key, and a GitHub repository or a base URL)mygo buildlinks the manifest URL of the target and the public key into the app, and when the private key is available (MYGO_UPDATER_PRIVATE_KEYorupdates.privateKey) archives the app as installed (the bundle, else everything next to the executable) into<name>-<version>-<target>.tar.gz(Linux builds always get it, forinstall.sh), signs its SHA-256, and writesupdate-<target>.jsonwith the## <version>section of CHANGELOG.md as notes.Updater.Checkfetches the manifest (HTTPS only, loopback HTTP for tests) and compares versions semantically;Update.Installstreams the archive next to the app, verifies size and signature, unpacks it (files, directories and relative links only) and swaps it in: the bundle is renamed on macOS, the entries of the app directory on Linux and Windows, where the running executable is renamed away and removed at the next launch. There the app directory holdsupdate.FileList(.mygo-files), its top-level entries as installed, one per line:mygo buildwrites it into the app (so the archive, installer and packages have it), andswapFilesrewrites it in the update with the update's entries before swapping it in with them. The entries that the installed list names and the update lacks are renamed aside first, as replaced ones are, and deleted once the swap succeeds, so a failed swap puts them back; entries that no list names (the user's, the uninstaller) and all of an app without a list stay, and so does the running executable. Deltas never make the list from the installed one, which the updater rewrote.App.Relaunchthen starts the new version from the path the app started from. Development builds are never updated. Delta updates work as Sparkle's (internal/update/delta.go):mygo buildfetches the published manifest of the target, downloads the archives of up toupdates.deltasversions (that manifest's, and those it lists asprevious, which the new manifest passes on), checks their signatures and writes a signed delta from each. Its index lists the tree of the new app, inside the bundle on macOS, with each file's size and SHA-256 and how to make it: a copy of a file of the old app with the same content (at its path, or elsewhere for moved files), else a patch of the file at its path or its own bytes, whichever is smaller. The stapled ticket (Contents/CodeResources) is never made from the old app: the archives' bundles are stapled, but the bundle a user copies from the disk image is not, asbuildDMGstaples the app after copying it into the image. Patches are bsdiff's (bsdiff.go, with the qsufsort suffix array ofsuffix.go), their three streams compressed apart so that they are read from the delta file in place.Update.Installtakes the delta whosefromis the running version, makes the new app next to the old one throughos.Roots, and fails on any file that does not match, then downloads the archive; the delta also names the versions it updates and makes. The tree made is the signed one byte for byte, so bundles keep their code signature.mygo build -uploadpublishes toupdates.githubwith theghCLI: it creates the release<tagPrefix><version>as a draft with the changelog section as notes, uploads installers, update archives and deltas, then the manifests, and leaves publishing the draft (which makes the manifests "latest") to the developer once every platform is there. With atagPrefixother thanvthe repository may hold other releases, so apps are built with a feed naming the manifest in the release<tagPrefix>{version}(update.TaggedFeed), whichupdate.ResolveFeedturns into the manifest of the newest published, non-prerelease release with that prefix, from the GitHub API's list of releases, newest first.Updater.Checkand the delta step resolve it in Go, andinstall.shin sh (latest_tag, which reads thetag_name,draftandprereleasefields in order). Such drafts are made with--latest=false, and published the same way, so the repository's latest release stays the others'. While the app turns prereleases on (Updater.SetPrereleases, anatomic.BoolthatCheckreads once),Checkresolves the feed of the latest release (with the prefixv) and tagged feeds alike withupdate.ResolvePrereleaseFeedinstead: the manifest of the release with the highest version (update.Compare) that is not a draft, among the pages of releases up to the one holding the newest release that is not a prerelease. Withupdates.s3it puts the same files, manifests last, into a bucket of S3 or a compatible service (cmd/mygo/s3.go): onePUTper object, signed with AWS Signature Version 4 in pure Go (crypto/hmac), its SHA-256 payload hash checked by the service; the bucket goes in the host name on AWS, and in the path with a customendpointor a bucket name with dots, unlesspathStylesays otherwise. An app with an update service of its own givesUpdater.SetSourceanUpdateSource(anatomic.PointerthatCheckreads once):Checkreads the manifest from itsManifestin place of the feed, and theUpdatekeeps the source, whoseOpendownloads the archive and deltas in place ofhttpGet. The configured feed is itself a source (feedSource), so parsing, sizes, signatures, staging and the swap are the same either way;Enabledstill needs a build withupdates, for the public key. - File associations (
fileAssociationsin the configuration) are declared by the packages (see the table below) and their extensions linked into the binary; like deep links, files of those extensions among the launch arguments and those a second instance forwards reachOnOpenFile(paths relative to the working directory, and file URLs from%U). - Downloads (
downloads.go): links withdownload, and responses a page cannot show or that are attachments, become downloads.OnWillDownloadgets the URL, the suggested name and a default path in Downloads (a unique name), which it may change or cancel;OnDownloadDonereports the result. WebKit cannot turn what a custom scheme handler serves into a download, and WebView2 would fetch it over the network, so those are canceled and the core serves the URL again through the scheme's handler into the file (SchemeDownload). - Permissions (
permissions.go): the backends route camera, microphone, location and notification requests toWindowHandler.PermissionRequested;SetPermissionHandlerdecides them, and by default trusted pages (the app's own,TrustedOrigins) are granted and others denied. Custom scheme pages are secure contexts with a real origin on every platform, which these APIs need.macos.infoPlistadds keys such as the camera and microphone usage descriptions macOS requires. - Sign-in and certificates (
auth.go): the backends route HTTP authentication challenges (Basic, Digest, NTLM, Negotiate, the server's or a proxy's) toWindowHandler.AuthRequested, whichSetAuthHandleranswers with credentials, or none to get the server's refusal; without a handler the answer isDefault, the web view's own handling. Server certificates that do not verify go toWindowHandler.CertificateError, false unlessSetCertificateErrorHandlerallows them; macOS checks certificates itself (SecTrustEvaluateAsyncWithError) only whileHandlesCertificateErrorsis true, and Linux and Windows extract the certificate only then. Client certificates go toWindowHandler.ClientCertificateRequested, whichSetClientCertificateHandleranswers with one of the offered certificates or an empty response to refuse; without a handler the answer isDefault, the web view's own handling. Linux lists no certificates to offer (WebKitGTK has none), so a handler there can only refuse, and a refusal ends the load as a cancellation, whichOnDidFailLoaddoes not report. macOS lists the keychain's identities only whileHandlesClientCertificatesis true, for the keychain asks the user for them. - Active windows (
active.go): a window is drawn as active, the one the user's input goes to, while it or a flyout of it (through flyouts' parents) has the keyboard (windows.focused), always whileSetActiveAppearancekeeps it so, and so are panels and flyouts without a parent; a flyout with one is as active as its parent, and a window that never had the keyboard nor showed without it (focusKnown) counts as active, so that the first frame of a window about to take it draws active, which packageuiassumes too. No backend reports it:FocusedandBlurredpostupdateActiveonce (scheduleActive), which works out every window's state after the native event returns, so that a window blurred by its flyout taking the keyboard in the same event (AppKit's resign and become key, Win32'sWM_ACTIVATEs, GTK's focus events before an idle source) never shows inactive for a frame. A change reaches native UI throughsurface.Conn.ActiveandActiveChanged, and pages, and those of the window's web views, as themygo:window-activeevent (true or false); pages start active, and one whose DOM is ready in an inactive window is told so. The bridge keeps it for mygo-runtime'swindowActive(), telling listeners of changes only, and sets thedata-mygo-window-inactiveattribute of<html>while it is false. The system draws the materials itself: macOS'sNSVisualEffectViewof a window follows AppKit's active state (NSVisualEffectStateFollowsWindowActiveState), the main window's look, which agrees for flyouts (a panel never becomes main) and differs while a sheet or one of AppKit's own panels (Fonts, Colors) is key: the material stays active there while the content is not. Panels', flyouts' and those of windowsSetActiveAppearancekeeps active stayNSVisualEffectStateActive, as they did for all windows before;NSGlassEffectViewhas no such state and follows AppKit's. DWM draws Mica and Acrylic as a solid fill in a window it frames inactive: a window whose focusable flyout Windows activates, as a window of its own, keeps its frame active (keepsActive:WM_NCACTIVATEpassed toDefWindowProcas active, and its drawn caption), until the activation leaves the flyout for another app's window (ownersInactive,flyoutLeft). GTK styles a window:backdropby GDK's focus, which a focusable flyout takes from its parent: the window's own title bar then shows it, while its content stays active. In browsers, a window is active while its canvas has the focus. - Flyouts (
flyout.go,internal/platform/flyout.go):NewFlyoutcreates a window withplatform.WindowOptions.Flyout, which backends make a borderless, transparent popup owned by its parent, and place withWindow.PlaceFlyoutagainst an anchor in the parent's content area.Flyout.Placeresolves the placement in the work area of a display as GDK does forgdk_window_move_to_rect(maybe_flip_position, then slide, then resize, per axis), andWorkAreaForpicks the display as GDK'sget_monitor_for_rect; macOS, X11 and Windows (in physical pixels) use it, Wayland hands the anchor to the compositor's positioner. The core places a window's flyouts again as it moves or resizes (owned windows on Windows and X11 popups do not follow), and asSetAnchorandSetSizechange them. A focusable flyout closes throughWindowHandler.ShouldClosewhen it loses the keyboard: as its panel resigns key (macOS), on a press outside it or a broken grab, or as the compositor dismisses the popup (GTK, asGtkMenu), and as another window is activated (Windows), whose activating click its owner eats (MA_ACTIVATEANDEAT), as menus' and WinUI's flyouts'. On macOS a local monitor eats a press on a panel's anchor, which only closes it, asNSPopovereats one on its positioning rect: the press would make the parent key, which closes the flyout before the release clicks the anchor and opens it again. An Escape the content leaves closes a flyout too: native UI reports one no shortcut took (surface.Conn.Escape), neither one of the last frame (routeKeys) nor one the frame handling the key builds, which takes it in the next frame (settleKeys): a key pressed as the view changes, before a frame shows the change, goes where the view now sends it, so an Escape typed ahead of the frame that builds a flyout's shortcut for it never closes the flyout. The bridge reports one no handler of the page cancelled (anescapemessage).Flyout.Popovershows one in anNSPopover(internal/darwin/popover.go), whose window AppKit makes as it shows, and whose transient behavior askspopoverShouldClose:.Flyout.Trayanchors one to a tray icon'sBoundsinstead, without a parent: the panel atNSPopUpMenuWindowLevelor anNSPopovershown from the status item's button (macOS), a topmost unowned popup (Windows), and on Linux, whose AppIndicator tells no bounds, an undecorated utility toplevel in the middle of the work area, which a focusable one closes as it loses the keyboard. A press on the icon does not dismiss its flyout (tray.pressedon resigning key, the cursor overShell_NotifyIconGetRecton Windows), so its click toggles it, but a popover's: AppKit eats a click on a transient popover's positioning view, the status item's button, whateverpopoverShouldClose:answers, so the button sends no action, and the click closes the popover (AppKit asks on the release on macOS 26, on the press elsewhere: one kept open there stayed open for good, #1050). A global monitor of mouse downs closes macOS's focusable panels on clicks outside the app, which a panel taking the keyboard of an app that was not activated never hears as the key window changing. A window modal to a flyout (WindowOptions.Parent,Modal) is centered over it by the core (overFlyout), in its display's work area, and keeps it open: the flyout's dismissals skip a flyout a window is modal to (modalChildon macOS and Linux,hasModalon Windows, whereEnableWindowdisables the owner as for any modal window). macOS attaches the window as an AppKit child window at the flyout's level rather than a sheet, and a local monitor eats the flyout's mouse and key events while it shows (watchModalInput);popoverShouldClose:keeps a popover. Windows makes it topmost over a topmost flyout. On X11 the popup lets its grab go while the window shows and grabs again as it hides (enterFlyout,leaveFlyout); a tray's flyout, a toplevel, does not close as it loses the keyboard to it. The design follows GPUI'sWindowKind::AnchoredPopupand winit'sWindowType::Popup, which model xdg_positioner too. - Panels (
WindowOptions.Panel), the windows of launchers: the core makes them always on top, out of the taskbar, unable to minimize, maximize or go full screen, and closes them on an Escape the content leaves, as flyouts. macOS makes aMyGoPanel, anNSPanelwith the nonactivating style frominitWithContentRect:on (the window server learns that a window never activates its app from the style it is created with; a style set later needs the private_setPreventsActivation:, as Electron and tauri-nspanel found), never the main window, on every Space as a full-screen auxiliary, outside Mission Control and the window cycle, at the floating level (above it, input methods' candidate windows would show behind the panel);ShowandFocusmake it key withoutactivateIgnoringOtherApps:, and itscancelOperation:does nothing (NSPanel's closes it pastwindowShouldClose:). The key window of a nonactivating panel gets the keys and the main menu its key equivalents, though the app is not frontmost (NSApp.isActivereports it active then). Windows makes a topmost tool window owned by the hidden application window (a tool window alone may get a button on other displays' taskbars, as PowerToys found), its corners rounded as Windows 11 rounds other windows'; one the app maximizes fills the work area (maximizeInWorkArea), where Windows would give a window without a maximize box the whole monitor, over the taskbar; the key press of aRegisterHotKeyshortcut lets the process take the foreground, and hiding the panel gives it back. Linux makes a sticky utility window, kept above, or on Wayland a surface of the overlay layer (internal/linux/layer.go):libgtk-layer-shell.so.0is loaded with the first panel, if it is there (0.9 patches GTK 3's private structures and intercepts nothing of libwayland, so it may load after GTK), and used wheregtk_layer_is_supportedsays the compositor speaks wlr-layer-shell, as Ulauncher does.gtk_layer_init_for_windowmust come before GTK realizes the window. A position is the top and left margins from the edges of the output there (gdk_display_get_monitor_at_point), and without one the compositor centers the surface; gtk-layer-shell sizes it by its size request, not its default size. It asks for the keyboard exclusively while it shows: compositors give none to an on-demand surface as it maps, and Sway takes it back from one that goes from exclusive to on demand. Neither Linux nor Windows shows the application menu in a panel. window.open()andtarget=_blankgo throughSetWindowOpenHandler. By default http(s) URLs open in the default browser. Allowing one creates a window around the configuration or related view WebKit provides, with its own content manager so scripts and messages never leak between windows. On Windows the request's deferral holds the popup until the new window's web view is set up (its scripts, handlers and filters), which then becomes the popup (put_NewWindow) before WebView2 navigates it, so the page keeps its opener. On Linux the new window loads the URL as an independent page.UserInitiatedsays whether a user's gesture made a request, and so doesNavigateEventfor navigations: on macOS WebKit's private-[WKNavigationAction _isUserInitiated](looked up withrespondsToSelector:; without it, a link or a form'snavigationType), which sees a click whose handler callswindow.open()or setslocation.href; on Linuxwebkit_navigation_action_is_user_gesture, though WebKitGTK refuses awindow.open()without one itself before asking (javascript-can-open-windows-automaticallystays off), so the handler sees those only on macOS and Windows; on Windows the event arguments'get_IsUserInitiated, WebView2 having no pop-up blocker. Only main-frame navigations reachOnWillNavigate: on Linux the action's target frame says so (webkit_navigation_action_get_target_frame_info, 2.56+, bound when present); before it WebKitGTK tells no navigation's frame, andmainFrameNavigationtakes all for the main frame's butabout:srcdoc, which only an iframe shows.
Menus
Menu/MenuItem are plain Go values built from templates. Roles expand into
labels, accelerators and submenus per platform (roleDefaults,
roleSubmenu); macOS-only roles are hidden elsewhere. Items get a process
unique id and are registered with weak pointers, so discarded context menus
can be collected. The core sends immutable snapshots (platform.Menu) to the
backend, which:
- builds native menus and tracks native items per owner (app menu, window menu
bar, tray, popup) so
UpdateMenuItemcan change label/state in place and rebuilt menus release their items, - performs edit roles natively (first responder on macOS,
webkit_web_view_execute_editing_commandon Linux; on Windows a page keeps their keys, which WebView2 performs, and the menu items rundocument.execCommandthrough DevTools, except Paste, which runs Chromium's paste command with the clipboard's formats, and Paste and Match Style, which inserts text), or sends them to a window showing native UI as aSurfaceCommand, and reports everything else throughWindowHandler.MenuItemClickedfor a window's menu, orAppHandler.MenuItemClickedfor menus without one; the core toggles checkbox/radio state, performs window and view roles and callsClickwith the menu's window, even while a submenu has focus instead, - enables the edit roles' items of native UI as the content answers a
SurfaceCommandEnabled(package ui'scommandEnabled, which followseditEvent): on macOS the menus holding them validate their items (autoenablesItems), the surface answeringvalidateMenuItem:and the menu target keeping the app's state, which a disabled role's item targets; Windows and Linux ask as a menu opens (WM_INITMENUPOPUP, a submenu'smap, a popup before it shows), and again before a role's shortcut runs.
Item icons (MenuItem.Icon, made by NewMenuIcon) travel as a
*platform.MenuIcon, which the core shares between the icons of the same
PNG (interned by content, held weakly): backends tell a change by the
pointer and decode an image once (MenuIcon.Native), freeing what they
made with runtime.AddCleanup or with the menus that show it. Their code
is linked by NewMenuIcon (LinkMenuIcons), not by the backends' menus.
Backends that keep a snapshot to build menus from later (Windows' trays,
the application menu of new windows) keep it current with the updates they
get (Menu.Apply).
macOS gets a default menu bar (App, File, Edit, View, Window), which is what makes Cmd+C/V/Q work; other platforms get none unless the app sets one.
A page's own context menu (context_menu.go, Page.OnContextMenu) goes the
other way: while a listener is set (WindowHandler.HandlesContextMenu), the
backend describes the menu the web view is about to show as a
platform.ContextMenu whose items carry a Native number and a Name,
and asks WindowHandler.ContextMenu. The core makes a Menu of template
items of them (Menu.transient, which refreshes no installed menu), fires
the listeners, and answers Default when the snapshot still shows the
web view's items as they were, none when prevented, else the snapshot,
each native item at most once. A native item the app gave a Click
leaves the snapshot as an item of the app's. What the backend does not tell
of the target comes from the bridge's context-menu record of the page's
contextmenu event, which arrives just before the backend's request on the
same connection, and is taken only when its point, times the page's zoom,
is the menu's (a frame's click never reaches the page's document); a new
page drops it. macOS and Linux change the native menu in place; Windows
draws a changed menu itself (see Platform
differences). Its code is linked by OnContextMenu.
Clipboard and drag data (transfer)
transfer.Data is the immutable serialized model shared by clipboard writes
and native drags: ordered items with alternative byte representations, MIME
formats, file/URI lists, and lazy providers. Each clipboard write or drag
starts an independent provider cache; discovery reads no bytes, and success,
errors and recovered provider panics are cached once. Typed Drag(value)
remains process-local, resolved by the core's temporary drag registry; its
token is rejected by clipboard writes.
clipboard.go owns validation, main-thread dispatch, provider reentry guards,
and deferred, exactly-once release hooks. platform.Clipboard accepts a
snapshot, returns materialized reads, discovers formats without rendering,
and flushes/closes native ownership. A concealed write
(ClipboardOptions.Concealed) carries each platform's mark for clipboard
managers next to its data, on write and on flush, and macOS writes it for
the current host only. Clipboard providers belong to the app,
not a source window. Quit flushes them before stopping the loop, and finish
releases remaining providers. Linux requires a clipboard manager for data
to remain after exit; a failed provider prevents flushing its offer.
macOS owns NSPasteboardItemDataProvider objects until AppKit finishes them, with eager NSPasteboardItems after flush. GTK uses application-owned selection get/clear callbacks and owner-change generation checks; clipboard-only apps bind selection primitives without requiring a surface. Windows shares the drag adapter's IDataObject, vtables, native conversions and reference-counted objects with OleSetClipboard/OleFlushClipboard; foreign reads hold a Win32 clipboard lock. Callback signatures are allocated once and routed by native object or user data. Native reads copy bytes so application data outlives native handles. See Clipboard and drag data for API and format mappings.
Native UI's Context.WriteClipboardThen writes its text with WriteData
and posts the error to the main thread for the next frame. A browser's
clipboard answers later: the web backend's is a platform.LaterClipboard,
whose WriteTextThen keeps the text for the page's own pastes and
answers once navigator.clipboard.writeText settles (it rejects without
transient user activation, and outside secure contexts there is no
navigator.clipboard to write).
Audio (audio)
Package audio is independent of windows and the main thread: it neither
goes through internal/platform nor needs the app running. Players are
voices (audio/mixer.go) under one mutex. Each reads frames from a source,
an in-memory sound or a stream that a goroutine of its own decodes about a
second ahead into a ring (audio/source.go); the mixer never waits on a
stream, playing silence for one that is late. Voices resample to 48 kHz
stereo at their rate times their speed, and ramp their gains (a 2×2 matrix
of volume and pan) over 5 ms on every change, pause and resume. Looping
happens in the source, so a loop has no gap; the voice counts frames past
it modulo the length for Position.
Up to one source frame an output frame, a voice interpolates with a
Catmull-Rom spline. Faster (a sound above 48 kHz, or one sped up), a spline
would fold what is above 24 kHz back below it, so the voice interpolates
with a Kaiser-windowed sinc (audio/resample.go), a low-pass at the
output's rate: 32 output frames either side, -6 dB at 22 kHz, flat to
20 kHz and 80 dB down from 24 kHz, as many times wider in source frames,
and its cutoff as many times lower, as the source is faster. Its weights
are a bank the voice computes when its step changes, for 128 phases an
output frame (fewer source frames apart, the faster), from a table of the
kernel made once; it interpolates between the two phases either side, so a
frame costs two multiply-adds a source frame and channel, about 1% of a
core at 96 kHz. Past 8 source frames an output frame, the kernel stops at
256 frames either side: its window narrows to those, and its cutoff comes
down so that its stopband still starts at 24 kHz, as far as it can (to
48× it does). The voice keeps the frames around where it is in a ring of
512, stored twice over so that its kernel's are in a row, and reads its
source as far ahead as the interpolation reaches; Position stays where
the output is, not where the voice read to, and a seek clears the ring.
A player that preserves pitch (SetPreservesPitch) gets a stretcher
(audio/stretch.go) between its source and its resampler, which then
resamples at the rate alone. It is WSOLA: hops of about 16 ms, each a
Hann crossfade from the natural continuation of the last segment into a
new one, which the speed puts a hop times the speed further on and a
search within about 10 ms moves to where the waveforms match best
(normalized cross-correlation in mono, every frame of about 16 kHz, then
each frame around the best; the natural continuation itself when in reach,
so at 1× it passes the source through). Its buffers are allocated once,
the first time; it reads ahead what a step needs, skipping what fast speeds
leave out, reads silence past the source's end and fades out once the
speed puts the next segment there, and tags each
frame it gives with the source frame the speed puts it at, which the voice
keeps beside its four frames of history for Position. Turned off, it
passes the source through from the natural continuation, and a voice keeps
it, so that voices that never preserved pitch pay one nil check.
Position is where a voice is heard, not mixed. The mixer counts the
frames it mixed and, of the latest, those the device holds: as many as it
was written, up to its latency, from when it opened (setHeld). Each
voice marks the output's frame and its own source frame where each mix
starts and ends, and forgets the marks the output played; Position
interpolates them at the frame heard, which carries speed changes, a
stream waiting for its decoder, and a pause playing out. A seek or a
stop drops them, so the position is the target at once and stands there
until the output plays what it mixed since.
The output (audio/output.go) is a goroutine locked to one thread that
opens the device when a voice starts, mixes 10 to 21 ms at a time and
writes it with a call that blocks while the device holds enough, and
closes the device after five idle seconds. audio.SetLatency chooses how
much the device holds ahead, 40 ms or 250 ms; the output opens it again
when that changes. Each platform gives one openDevice, which takes it:
- macOS: an Audio Queue of buffers of 512 frames, as many as hold that (4 or 24), started once all hold audio; its callback, created once, hands buffers back over a channel. A theme change can keep every thread of an app waiting for up to 0.2 s, which 4 buffers do not ride out.
- Linux:
pa_simple_writeof PulseAudio's simple API (libpulse-simple, which PipeWire serves too) with that as its target, else ALSA'ssnd_pcm_writeiondefault, both through purego. - Windows: WASAPI in shared mode, event driven, with a buffer of that,
AUTOCONVERTPCMand the default-quality resampler; it compares the default endpoint's ID every 500 ms and fails the write when it changed, which reopens the device. - Browsers: an AudioWorklet from a Blob URL that queues chunks of 1024
frames (three, or twelve) and posts a request whenever it used one; the
output waits for a request before mixing the next, and asks more or
answers fewer when the latency changes. One AudioContext serves the
page, with the
latencyHintit began with; closing suspends it, and the first pointer or key input resumes it.
Where openDevice fails, a null device takes the frames on the wall
clock and fails after two seconds, so the output tries the device again;
audio.Err reports the failure. Formats register a magic prefix and a
constructor as image does: WAV is built in, audio/vorbis and
audio/mp3 wrap pure Go decoders, and so do audio/aac and audio/flac.
A file that starts with an ID3 tag is matched by what follows it.
audio/mp4 reads the container and leaves its packets to a codec
registered by the sample entry's type (mp4.RegisterCodec): ALAC, an
in-package port of Apple's decoder, mp4a from audio/aac, fLaC from
audio/flac, so that the LGPL of go-aac stays with the apps that import
audio/aac. It places packets from the sample table, or, of a fragmented
file, from each moof's trun as playing reaches it, with the length
from the sidx; AAC's packets it places 1024 frames apart whatever their
durations say, since that is what each decodes to, and a seek decodes one
packet before the one it lands in, whose sound overlaps it.
Watching files (watch.go)
WatchFiles keeps what it saw of its directory (watch_scan.go): for
every file and directory, by path, a fileWatchEntry of 40 bytes without
pointers (the identity os.SameFile compares, size, modification time and
mode) rather than its os.FileInfo, whose syscall.Stat_t the collector
would scan: 220 to 260 bytes an entry, with the path and its name in its
directory's set. It is read through an os.Root so that a path can't lead
out of it, and symlinks are not followed.
Each directory is opened as an os.Root of its own from the one that has
it, and what is in it is looked at by name relative to it, one call each,
as watchman and @parcel/watcher crawl with openat and fstatat: a
root opens every directory of a deeper path in turn. On Unix, reading a
directory opened in a root stats its entries already
(fileWatchEntryInfo). Looking at everything again lists each directory
from the one that has it. A directory replaced by a symlink, which a root
follows only within itself, is a replacement, wherever it leads.
The backend's watcher (platform.FileWatch) is given targets, the entries
the core enrolls, and reports notices: a target, or a child of a directory
target by name, changed. A notice says where to look, not what happened:
after the debounce, the core lists again the directories that notices
named and stats the files, and the events are the differences between what
it sees and what it saw (fileWatchDiff). An entry whose identity
(os.SameFile) moved is a rename, unless a hard link makes it ambiguous,
and what moved with a directory renamed above it says nothing more. A
file replaced at its path by another file, as an atomic save replaces it,
is a write, not a removal and a creation: apps that follow a document
would take it for deleted. The
entries are paired by looking their identity up (fileWatchIDs: device
and inode, or on Windows the volume and file index os.SameFile reads),
so a directory of n files renamed costs n lookups, not n² comparisons;
an entry whose identity can't be read is compared with every other one. When
the system lost changes (an inotify queue overflow, Windows'
ERROR_NOTIFY_ENUM_DIR, FSEvents' MustScanSubDirs or a dropped event),
the core lists every directory again rather than stopping. Enrolling is
checked against the scan: the backend opens a target
and compares its identity with what the scan saw, then the scan stats it
again, so that an entry replaced in between (an editor's atomic save) is
looked at again rather than watched in place of the other.
A watched file is watched through its directory, enrolled as .. with
the file as .: the scan's os.Root is the directory, and notices of the
directory naming other entries are dropped. A file replaced at its path is
reported as written. Ignore is asked from the directory listing's
type, before an entry is enrolled, so an ignored directory is never opened;
the directories it ignored are kept per listed directory, so that the
notices of a Subtree target about what is in them are dropped.
HintsOnly swaps the scan for fileWatchHints (watch_hints.go), behind
the same fileWatchLooker the watch's goroutine drives: no snapshot, only
the directories it enrolls. A notice naming an entry hints at the
directory holding it; one naming an enrolled directory, or the directory
itself, has the next update check that it is still what was enrolled and,
if not (moved, removed, replaced), drop the targets under it; a name that
may be a new directory is enrolled with what is beneath it. An overflow
hints at . and enrolls the tree again. Hints under one another collapse
into the shallowest. On Linux the directories are listed with getdents
types (watch_hints_linux.go), as notify walks them, so files are never
stat'ed; elsewhere a recursive watch is the root's Subtree target alone.
It is opt-in because it gives up what the snapshot is for: exact events,
renames, writes told from metadata, and which file.
An entry the scan can't look at, a directory without permission or a
file Windows holds (pagefile.sys, a sharing violation), is left as it
was seen, if at all, and what is in it unwatched, rather than ending the
watch; only the watched directory itself must be readable.
Next returns a batch, the events of one update. An update doesn't run
while a batch waits for Next (taken tells the goroutine when one was
taken): notices still mark what to look at, a set that falls back to
looking at everything past MaxEntries, so a slow reader loses nothing
and holds one batch. A directory gone ends the watch with a FileRemove
of . and io.EOF, as Close and the watch's context do.
The watch's goroutine is the only user of its scan and of its backend's
watcher, whose methods internal/watchdriver runs on a goroutine of its
own: it adds and removes targets between waits for the system, which
Wake interrupts, and calls the core's notice with what the system
reports; notice hands it to the watch's goroutine over a channel, and a
full channel makes the watch look at everything again. Neither uses the
main thread, so watches work before the app runs and don't wait on it.
- macOS: an FSEventStream per directory target, which covers the whole
tree beneath it, so nothing is opened per file, and a file target is
covered by the stream of its directory. A target below a stream already
watching its tree needs no stream of its own, which keeps a watch of a
directory that isn't recursive at one stream. The stream names what
changed (
kFSEventStreamCreateFlagFileEvents) and hears of the root itself (kFSEventStreamCreateFlagWatchRoot), delivering through one callback created at startup, on one serial dispatch queue of the process, never the main thread.Subtreetargets report paths at any depth; one without it drops what changes deeper than its entries. The stream's path is resolved withrealpath, as FSEvents names what is on disk, which a path given on a case-insensitive volume may not. The notices are hints:MustScanSubDirsor a full queue makes the core look at everything again, as an overflow does elsewhere, and the stream's 10 ms latency is under the core's debounce, which does the coalescing an app sees; withkFSEventStreamCreateFlagNoDeferthe first event after a quiet period comes at once, and those that follow it within the latency are batched. The stream startsSinceNow, but fseventsd can report a change made just before it started; the core's snapshot is taken as targets are enrolled, so nothing comes of it. - Linux: one inotify instance for the process, as fsnotify, with a watch
per directory, which names the child that changed: watches cost watch
descriptors, not instances, of which many systems allow 128 per user
for all apps. A goroutine reads it through the runtime's poller and
queues each watch's notices, which its driver's
Waitreturns. The same directory added twice, by one watch or several, shares a watch descriptor, and a removed descriptor's events still queued before itsIN_IGNOREDmust not be taken for a new one's, so the backend keeps generations of targets, each its watch and ID, per descriptor; the last target of a descriptor removes it. - Windows:
ReadDirectoryChangesWon overlapped handles, completing on a completion port per watch. Windows refuses to rename a directory while a handle is open on something beneath it, so a recursive watch is oneSubtreetarget on its directory, which reports paths; other backends returnErrUnsupportedforSubtreeand get every directory. The directory's own handle says nothing when it is renamed, so the core also watches its parent and looks at the directory when the parent names it. The handles can't be given to the os package, which would tie them to the Go runtime's completion port. A handle closed still completes its I/O, which writes to its buffer, so a session lives until its completion. A drive's root has no parent to watch, and can't be renamed. The directories thatLstattakes for directories are watched, which leaves out junctions and mount points but not the placeholders of cloud files (OneDrive), reparse points that are no name surrogates.
Backends report a write (FileWatchWrite) only for content: FSEvents'
ItemModified on a file (its ItemCreated, which APFS also sets on an
O_TRUNC write, stays dirty and lets the core's comparison report the
change), inotify's IN_MODIFY and IN_CLOSE_WRITE,
and Windows without FILE_NOTIFY_CHANGE_ATTRIBUTES. Attribute changes are
dirty, and the core compares only size and modification time, so that
indexers and antivirus touching metadata report nothing.
Documents (document*.go)
NewDocuments follows NSDocument and NSDocumentController for the
bookkeeping and SwiftUI's 2026 Document for the shape (a snapshot on the
main thread, the I/O on goroutines), with the recovery copies of VS Code
and Zed. Everything is in the core; backends only show a window's
document (Window.SetRepresentedFile, SetDocumentEdited) and replace
files (Backend.ReplaceFile). The menus reach documents only through the
functions the first kind registers (linkedDocuments), so that apps
without documents do not link them through the File menu's roles and Open
Recent (about 290 KB).
- State.
Document[T]embedsdocBase, the state that doesn't depend on the content's type, under one mutex: a change counter (changes), the change the file has (savedAt) and the recovery copy (backedUp), the edited flag, and the file's stamp (modification time and size) as last read or written.docOpsis what the generic half gives the rest:capturetakes the content and its change (on the main thread with aSnapshot),markSavedmakes a written snapshot the saved one (withEqual, the document stays edited only if the content differs), so edits made during a write stay unsaved, as NSDocument's change count tokens and Zed'sdid_savekeep them. - Writes of a document's file and of its recovery copy hold its
iomutex, which checks of the disk take too, so a watch never mistakes the document's own write for another app's.writeFileSafelywrites a sibling, flushes it and replaces the file through the backend (NSFileManager,ReplaceFileW, rename with mode and owner on Linux), in place for hard links and folders the user can't create files in. Save and autosave stat the file first: a stamp other than the one known is a conflict. - Pacing. One timer per document fires after the edits stop: the
autosave delay for a document saved in place, a second for a recovery
copy, shortened so that a document edited without a pause is written at
least every 30 or 5 seconds (
dirtySince). - Recovery copies live in
PathUserData/Recovery/<kind>/<session>, a session per running instance with an index of the copies, locked withflockorLockFileExwhile the instance runs; at launch an instance takes the sessions whose lock it gets, reopens their documents edited (in conflict when the file's stamp changed), writes them into its own session, then removes only the old copies it restored. A copy it skips or cannot read, and one whose new copy cannot be written, stays for the next launch. - Watches. One
WatchFilesper file of open documents, shared by reference count, which costs the same whatever else its folder holds: a watch covers the folder, not each file in it. A batch makes the document look at the disk (checkDisk), as focusing its window does, and so does the watch once it started. A removal looks in the folder for the file's identity, to follow a rename. - Closing and quitting never nest an event loop:
OnCloseof a document's window prevents a close that must save or ask, does it on a goroutine (dialogs are sheets on macOS), then destroys the window. The quit'sOnBeforeQuitlistener prevents a quit with edited documents, saves them in place, writes their recovery copies or asks, then callsApp.Quitagain (docQuit.resolvedlets that one through), and windows closing during a quit no longer ask. - Launch.
whenLaunchedruns once the launch arguments were delivered: the kinds restore their recovery copies, the files queued byOnOpenFileopen, and then, with no document open, an untitled one. - Menus. The document roles go to
documentRolebefore the window roles;refreshDocumentMenusenables them for the focused window's document when focus or a document's state changes, andupdateRecentMenusrebuilds the Open Recent submenus fromrecent-documents.json(recent_docs.go), whichApp.AddRecentDocumentandClearRecentDocumentswrite, for documents and other apps alike, before telling the system; in an app without documents an item goes toOnOpenFile.
Native UI (ui)
The public API keeps one stable *Context per window. Child builders share
it and temporarily change its parent. Element is a 16-byte checked value:
a direct owner record, arena slot and 32-bit generation. The owner detaches
from the engine on close and retires before generation wrap. Old elements
cannot alias recycled nodes. A closed window keeps nothing of what it
showed (#1281): mygo.Window keeps only that it shows Content, handing the
Content to its surface as it attaches, and the closed engine drops its view,
the last frame's elements, states and scene, and the host its last scene,
renderers and placements, in case anything still holds them. Development builds and Tester diagnose stale
use; production methods return empty results or ignore it. Handle stores
persistent control identity separately for each window. Focus queries read
identity without depending on construction order; focus requests wait for a
hidden control. Services offers persistent clipboard, URL and redraw access.
Constructors build and style controls eagerly. Context.Key supplies an ID
before state initialization. A control takes its input as the public
constructor that built it returns, as Dear ImGui's widgets do, so what it
binds, a local copy too, holds the edit when the next statement reads it
(value_input.go). The generated facade brackets each constructor with
beginBuild and endBuild; a control's queued input records how many
constructors were under way, and endBuild finalizes the input queued
deeper than the constructors still under way: a container's (a radio
group's arrow keys) waits aside until the call that built its children
returns, and each entry is visited once. Input queued outside public
constructors, by the engine's own controls, is finished at once, and what
remains applies after construction. Input follows the options of the build
the user acted on: a constructor resets its fluent options (ReadOnly,
OnPaste, a slider's Step) for this build after keeping the last build's,
which edits apply by (editor.withShown, sliderSettings.shown), and the
retained flagDisabled is last build's. Changed, Submitted, click and
shortcut queries apply finished input before answering.
ComboboxParts.Chosen applies input and reports a choice in the same
pass; it is not carried into a later rebuild. OnChange and OnSubmit run after construction and bound
input, before rebuilding. Notices are cleared before another pass so an
edit cannot be reported again on a fresh local binding. Callback actions
consume input once and rebuild before paint.
The private render tree uses node and context; internal/uigen generates
the checked public facade. FocusBind binds desired focus to app data; FocusedValue reads actual focus independently of a hidden
control's pending request.
See the migration guide for the breaking element API,
mygo migrate-ui, and the Go type-aware lifetime checks in mygo vet.
A window with WindowOptions.Content shows a user interface MyGo draws
itself instead of a web page. The layers stay as everywhere else: the
toolkit (ui) is plain Go above the platform contract, a backend only
provides a surface to draw on and its input, and renderers know nothing of
either.
view (Go) ──► ui: build, layout, paint ──► internal/scene ──► internal/gpu: d3d11 | metal | gl, or internal/raster
▲ └─ internal/text: DirectWrite | Core Text | Pango | canvas, glyph and mask atlases
│
internal/surface.Conn ◄── content.go (package mygo) ◄── platform.Surface: darwin | linux | windows | web | fake-
The surface. With
platform.WindowOptions.Surface, a backend creates a view MyGo draws in place of the webview: a layer-backed NSView on macOS, a GtkGLArea on Linux (a GtkDrawingArea where OpenGL would not run on a GPU), a child window of classMyGoSurfaceon Windows.platform.Surfacegives its native handles (for a swap chain, a layer, or the GtkGLArea while itsrendersignal draws a frame, with its context current), size and scale, and the refresh rate of the display showing it (RefreshRate: the screen'smaximumFramesPerSecond, GDK'sgdk_monitor_get_refresh_rate, the display mode's frequency fromEnumDisplaySettingsW); asks for a frame (RequestFrame: a pausedCADisplayLink, whose ticks draw frames that follow each other themselves while a frame after a pause draws at once, in the same pass of the run loop, from the backend's queue (drawSoon), as does the first frame of a window out of sight, which the link does not tick for (platform.FirstFrameSurface): a window made hidden draws it while hidden, which makes it ready to show, and a resize's when AppKit displays the view, before macOS 14 anNSTimerat the display's rate,gtk_widget_queue_drawor a tick callback of GTK's frame clock,InvalidateRect); presents pixels drawn on the CPU (PresentPixels: a CGImage as the layer's contents, cairo in thedrawsignal,SetDIBitsToDeviceinWM_PAINT; on LinuxPresentDamagetoo, with what changed); sets the cursor; and turns the input method on and off at the caret (NSTextInputClient, GtkIMContext, IMM32), with the text around it, up to 512 runes on each side (SetTextInput). Everything else comes throughWindowHandler.SurfaceEvent, in DIPs: frames, resizes, the pointer, the wheel, keys, text, compositions, focus, the edit roles of menus (SurfaceCommand), files dragged and dropped, and assistive technology. The back and forward buttons of a mouse come as the keysKeyBackandKeyForward, as do the keys of keyboards that have them: buttons 3 and 4 ofotherMouseDown:on macOS, andswipeWithEvent:'s swipes, left going back as in Safari, which touchpads swiping between pages send and mice's makers' software sends for their buttons (Logi Options+), buttons 8 and 9 andXF86BackandXF86Forwardon Linux, andWM_XBUTTONUP(taken, so that DefWindowProc sends noWM_APPCOMMANDas well),VK_BROWSER_BACKandVK_BROWSER_FORWARD, andWM_APPCOMMAND's browser commands on Windows. A key held down repeats asKeyPressedwithRepeat, which the shortcut that takes it tells (shortcutReg.repeat,Context.ShortcutRepeated):isARepeaton macOS (and the current event's, for the Edit menu's commands that reach shortcuts), bit 30 ofWM_KEYDOWN's lParam on Windows,KeyboardEvent.repeatin browsers. GdkEventKey flags none, so the Linux surface notes the hardware keycodes held (keysDown, before the input method sees the key): a press of one held already, with no release between, is a repeat, as X11 with detectable auto-repeat (which GDK turns on) and GTK's own repeat on Wayland send them. A key typed while the input method composes is the input method's alone, as Enter choosing a candidate: on macOSkeyDown:sends noKeyPressedwhile there is marked text, as GTK's input method filters such keys on Linux and IMM32 makes themVK_PROCESSKEYon Windows. A composition an input method gives up, as with Escape, goes: the Microsoft Japanese IME sendsWM_IME_COMPOSITIONwith neitherGCS_COMPSTRnorGCS_RESULTSTRthen, which empties the marked text (InputComposition.Cancel) beforeWM_IME_ENDCOMPOSITIONunmarks it. A composition the content does not take (SurfaceEventreturns false forTextComposition) macOS gives up (discardMarkedText, onceinterpretKeyEvents:returns), as the terminal leaves the dead key ´ of Option-E that it sent as Alt, so that the next key is not composed with it. Input methods name what text and compositions replace in the text around the caret (Replace,From,To): NSTextInputClient's replacement ranges, GtkIMContext'sdelete-surrounding, IMM32's reconversion (IMR_CONFIRMRECONVERTSTRING). Files dragged over the surface (NSDraggingDestination, a GTK drag destination, an OLEIDropTarget) areFileDragOverevents, whose answer the drag source shows, and a drop aFileDrop; files the content does not take go toOnFileDrop. On Linux the area takes the touchscreen's touches (GDK_TOUCH_MASK, in itseventsignal, before GTK'stouch-eventhandler makes the first finger the mouse), whichtouch.goturns into what the mouse and a touchpad do: a tap clicks where it landed, held still half a second it right-clicks, as Windows's press and hold; a finger moving past 8 DIPs scrolls (Precise) the content under it along with it, and two fingers zoom (PointerZoom) between them and scroll as their midpoint moves. Button and motion events from a touchscreen are dropped once the area has had touches. -
Assistive technology. Once it asks for a surface's content, the backend sends
AccessibilityOn, and the engine describes every frame (ui/access.go) as aplatform.AccessTree: nodes in pre-order with roles, names, values, ranges, states, bounds and actions, underUpdateAccessibility. Backends keep a native object per node ID and tell the system what changed between trees: subclasses ofNSAccessibilityElementand AppKit's notifications on macOS; on Linux, ATK objects of GObject types registered through purego below the accessible of aGtkGLAreaorGtkDrawingAreasubclass, with ATK's signals, which GTK bridges to AT-SPI; on Windows, UI Automation fragments, COM objects with control patterns, answeringWM_GETOBJECT, with UI Automation's events. What assistive technology does comes back asAccessActionevents, which the engine performs as the pointer or the keyboard would. An element ignoring input (IgnoreInput: a field of the node, as the flags are all taken, and of its state, which the elements inside it inherit at commit) adds no hits nor stop of Tab and drops the focus, but stays in the tree, its presses counted as any other's; it offers no focus, nor the increments that go through the keys. One hidden from assistive technology (AccessHidden, a field of the node and of its inherited state, asIgnoreInput; inert ones count too) paints and takes input, butaccessElementskips it with what is inside it, as an invisible one, its text names no ancestor (innerText), and its state drops whatAccessActionsends it. One only unavailable (AccessDisabled, ARIA's aria-disabled, a field of the node and of its inherited state too) keepsflagDisabledoff, and so its hits, its stop of Tab and its look, but its node isAccessDisabled, which the backends already map (AXEnabled, ATK's enabled and sensitive states, UI Automation'sIsEnabled), stillAccessFocusable, with no actions but the focus and scrolling into view, and its state drops the others; UI Automation'sSetFocusreaches a disabled element that offers the focus, as Chromium's does one aria-disabled.AccessTree.Announcementsare texts to read out once (Context.Announce, toasts, routers):AXAnnouncementRequestedposted on the application with a medium priority on macOS, AtkObject'sannouncementsignal (ATK 2.46), which AT-SPI carries asObject:Announcement, on Linux, and on Windows a polite live region at the end of the tree holding the text, raisingLiveRegionChanged, as Flutter's alerts:UiaRaiseNotificationEventreturnedS_OKbut its events reached no client, unlike the provider's automation events. Statuses, as toasts, are polite live regions too (LiveSetting). Widgets of an app's own set what the bases set with element methods (Checked,Mixed,Expanded,Value,Range,Level,ActiveDescendant,Busy,Current,AutoComplete,PositionInSet).AccessSelectedisSelectedwithout its accent background and text color: it sets the same state (checked, which isAccessChecked, read as selected on tabs, options, rows, cells and tree items), so every bridge, a table's or a grid's choice of cells and rows included, reads it as it readsSelected's.PositionInSet(accSet, aria-posinset and aria-setsize, each 0 for not given) wins over thesetPosandsetSizethe bases give their items, which count those built, but not over a List's rows, whose place among all its rows also places them in their table on macOS and Windows.Busy(AccessBusy, ARIA's aria-busy) is WebKit'sAXElementBusy, withAXElementBusyChanged, on macOS, ATK's busy state, andbusy=truein UI Automation's AriaProperties;Current(AccessNode.Current, aria-current's kinds) is WebKit'sAXARIACurrent, ATK'scurrentattribute with the active state, as Chromium's, andcurrent=pagein AriaProperties. Both bring a generic element into the tree as a group, as ARIA's global states do.AutoComplete(AccessNode.AutoComplete, aria-autocomplete's inline, list and both; none writes nothing) is WebKit'sAXAutocompleteValue, answered throughaccessibilityAttributeValue:, ATK'sautocompleteobject attribute with the supports-autocompletion state, as Core-AAM maps it (and as IAccessible2's object attribute and state), andautocomplete=listin AriaProperties, which UI Automation has for ARIA's properties without one of its own: Core-AAM maps nothing in UI Automation nor in AX API, and Chromium nothing in UI Automation;Combobox,Autocompleteand aTokenFieldwith suggestions set it to list.RoleDescription(AccessNode.RoleDescription, aria-roledescription) is AppKit'saccessibilityRoleDescription, which elements override, ATK'sroledescriptionattribute and UIA's LocalizedControlType;Controls(AccessNode.Controls, the IDs of the elements, aria-controls) isAXLinkedUIElements, ATK'scontroller-forrelation andcontrolled-byback, from the nodes'ref_relation_set, and UIA's ControllerFor;DescribedBy(AccessNode.DescribedBy, aria-describedby) adds the text of the elements toDescription(their Label, else the texts inside them, hidden or not, joined by spaces, after the element's own), and is ATK'sdescribed-byrelation withdescription-forback, and UIA's DescribedBy beside FullDescription; AX API has no relation for it, only the text inAXHelp.ErrorMessage(AccessNode.ErrorMessage, aria-errormessage) is given only while the element is invalid (InvalidsetsAccessInvalidalone, orError), puts the text first inDescription, asErrordoes its message, and is WebKit'sAXErrorMessageElements, answered asAXInvalidis, ATK'serror-messagerelation witherror-forback, and UIA's ControllerFor, as Core-AAM maps it; UIA also raises IsDataValidForForm and FullDescription changing, as a field turns invalid as it blurs.Orientation(aria-orientation) setsAccessVerticalorAccessHorizontalon any element:AXOrientation, ATK's vertical or horizontal state and UIA's Orientation, a slider and a separator across unless told, others without one.Searchon a text input setsAccessSearch:AXSearchField, ATK'sxml-roles:searchboxandtext-input-type:search, UIA's Edit with "search box" and the searchbox AriaRole.RoleDeletionandRoleInsertion(ARIA's deletion and insertion) areAXGroupwithAXDeleteStyleGroupandAXInsertStyleGroup, ATK's content deletion and content insertion withxml-roles, and UIA's Text named "deletion" and "insertion".RoleTabPanel(ARIA's tabpanel, the content a tab'sControlspoints to) isAXGroupwith the role description "tab panel", ATK's scroll pane withxml-roles:tabpanel, and UIA's Pane with "tab panel" and the tabpanel AriaRole;HasPopup(AccessNode.HasPopup, aria-haspopup) isAXPopupValue, answered asAXInvalidis, withAXShowMenupressing the element, ATK's has-popup state andhaspopupattribute, and UIA's ExpandCollapse withhaspopup=in AriaProperties;Sortsets a header'sAccessSortAscending,AccessSortDescendingorAccessSortOther(ATK'ssort:other, UIA's ItemStatus andsort=in AriaProperties, AppKit's unknown direction). A cell (RoleCellor a header) thatCheckedorSelectedmarks isAccessSelectable, chosen whileAccessChecked, as ARIA's gridcells with aria-selected, and the table around itAccessSelectabletoo:AXSelectedand the table'sAXSelectedCells(withAXSelectedCellsChanged), ATK's selectable and selected states rather than checked, UIA's SelectionItem in the table's Selection.RoleNavigation, a landmark (Breadcrumbs), isAXGroupwith the subroleAXLandmarkNavigation, ATK's landmark withxml-roles:navigation, and UI Automation's Group with the navigation LandmarkType and AriaRole. Native UI in browsers has no accessibility tree yet (UpdateAccessibilityis empty ininternal/web), so there is no ARIA to map them to. The other landmarks, live regions and rules map as WAI-ARIA's Core Accessibility API Mappings do:RoleRegionis one only with a name (a group without),AXLandmarkRegion, ATK's landmark withxml-roles:region, and UIA's custom LandmarkType;RoleSearchisAXGroupwithAXLandmarkSearch, ATK's landmark withxml-roles:search, and UIA's Group with the search LandmarkType;RoleMain,RoleBanner,RoleContentInfoandRoleComplementaryareAXGroupwithAXLandmarkMain,AXLandmarkBanner,AXLandmarkContentInfoandAXLandmarkComplementary, ATK's landmark withxml-rolesmain, banner, contentinfo and complementary, and UIA's Group with the main LandmarkType, or the custom one named "banner", "content information" and "complementary";RoleArticle, no landmark, isAXGroupwithAXDocumentArticle, ATK's article, and UIA's Group named "article";RoleLog,RoleTimerandRoleAlertareAXGroupwithAXApplicationLog,AXApplicationTimerandAXApplicationAlert, ATK's log, timer and notification, and UIA's Group named "log", "timer" and "alert";RoleSeparatorisAXSplitter, ATK's separator and UIA's Separator, vertical when taller than wide (AccessVertical, withAXOrientation, ATK's vertical state and UIA's Orientation);RoleRowHeaderisAXCell, ATK's row header and UIA's HeaderItem, named by its content but keeping what is inside it, as a cell does (ARIA's rowheader is not children-presentational). Live regions (AccessRole.Live) say how they are read:AXARIALive,AXARIAAtomicandAXARIARelevant, answered asAXInvalidis; ATK'slive,atomicandrelevantattributes, with theircontainer-forms on what is inside them; UIA's LiveSetting. Screen readers read the platforms' live regions in web content alone (WebKit's and Chromium'sAXLiveRegionChanged, Orca's web scripts), so the engine reads them out itself (ui/access_live.go): it compares each frame's tree with the last, announces what a log that showed gained, joined for each log, and puts the text an alert shows that no alert showed in the last tree intoAccessTree.Alerts, which interrupt:AXAnnouncementRequestedwith a high priority, ATK'snotificationsignal withATK_LIVE_ASSERTIVE(ATK 2.50, else an announcement), and an assertive live region beside the announcements' on Windows. Alerts are told apart by their text, as an element's ID shifts when others appear before it; the tree assistive technology first gets, and a log first showing, read nothing out. Menus drawn in the window are AppKit'sAXMenu,AXMenuBarandAXMenuItem, whose choice shows asAXMenuItemMarkChar(answered throughaccessibilityAttributeValue:, asAXInvalidis), ATK's menu, menu bar, menu item, check menu item and radio menu item, and UI Automation's Menu, MenuBar and MenuItem, with Toggle for the items showing a choice; headings are WebKit'sAXHeading, whose value is their level, ATK's heading with thelevelattribute, and UI Automation's Text withHeadingLevel. A vertical slider says so (AccessVertical:AXOrientation, ATK's vertical state, UI Automation's Orientation). A range's step, how far the keys move its value (AccessNode.Step), is UI Automation's SmallChange (and LargeChange at least as much) and ATK's minimum increment, throughget_minimum_increment, as purego's callbacks return no floats forget_increment; AppKit has none, and its increments go through the keys. A read-only text input isAccessReadOnly, withoutActionSetValue; a text box of your own (Role(RoleTextField)with no editor of MyGo's) says so throughReadOnlyandMultiline(aria-readonly, aria-multiline).ReadOnlyis reported by the roles aria-readonly is for, on elements without a widget of MyGo's behind them: text box, combo box, check box, switch, slider, stepper, radio group, list box, grid and tree grid, and the cells and headers inside a grid or tree grid (reportsReadOnly, the nearesttabularancestor decides; a table's cells do not have it). Such a node has noActionSetValue,ActionIncrement,ActionDecrement(norActionPresson a check box or switch); ATK addsread-onlyto the states of any role withAccessReadOnly, UI Automation'sIsReadOnlyfollows the state, and AppKit'sAXValueis not settable. AppKit finds an element's value settable when its class overrides the setter, whateverisAccessibilitySelectorAllowed:says, so elements answer the olderaccessibilityIsAttributeSettable:themselves, as AppKit would but for that value:NSAccessibilityElementhas no implementation to call. The keys that press or focus an element (KeyShortcuts, ARIA's aria-keyshortcuts) are formatted asuiwrites a menu's shortcuts (shortcutText) as the view builds, so that an app not giving any links no formatting, and reach the backends as one string (AccessNode.KeyShortcuts): WebKit'sAXKeyShortcutsValue, answered throughaccessibilityAttributeValue:, ATK'skeyshortcutsobject attribute and UI Automation'sAcceleratorKey, as Chromium's. The rows of aList(list items) and aTable(rows) are named by their content and say which of all the rows they are (PosInSet,SetSize, the list giving its total), as only those in view are built; rows built beyond the view areAccessOffscreen, and what is in a scroll container scrolls into view on request (AccessScrollIntoView), a row of a list by its place, which builds it. A list choosing rows has the focus for them: the tree'sFocusis the chosen row while the list has the keyboard focus, so the arrows are read as they move the choice, and focusing a row chooses it, as screen readers expect of an active descendant. On macOS, lists are tables (AXTable) of rows (AXTableRow), as AppKit's, withAXIndex,AXRowCount,AXVisibleRows,AXSelectedRowsandAXSelectedRowsChanged(a list without aSetSizeorRowCountof its own counting as many rows as its items'SetSizesays, if more than it holds), andAXScrollToVisible, an action of AppKit's older API, which elements then answer for all their actions; on Linux, a list choosing rows is a list box withAtkSelectionandselection-changed, rows carry theposinsetandsetsizeobject attributes (as any item's), andAtkComponent'sscroll_to(ATK 2.30) scrolls; on Windows, rows havePositionInSet,SizeOfSet,IsOffscreen,SelectionItem(raisingElementSelected) in their list'sSelection, andScrollItem. A list choosing several rows isAccessMultiselectable(ATK'smultiselectable, UIA'sCanSelectMultiple, rows then raisingElementAddedToSelectionandElementRemovedFromSelectionunless chosen alone), and its focus is on the row last chosen.RoleListBoxandRoleOption, a list box of an app's own (ARIA'slistboxandoption), areRoleListandRoleListItemwithAccessSelectable, as aComboboxBase's popup and its options (choosesItems,flagChoosable) and aSelectBase's are, so the bridges show them as the lists above, selected withChecked; unlikeflagChoosable's rows, they take the focus only whenFocusable. ASelectBase's options are selected while highlighted, the trigger'sActiveDescendantthe one highlighted, as a native select's popup: assistive technology focusing one (focusHighlights) notes it on its state (focusAsked), which the select highlights as it builds it, rather than clicking it, which would choose it. Grids and tree grids (RoleGrid,RoleTreeGrid) are tables to the backends (AccessRole.Tabular), whose rows aListshown as one makes rows rather than list items; a row of an app's own saying whether it is chosen (Checked,Selected) isAccessSelectable, as is the table or grid holding it, which holds its cells' choice too. The counts and indexes an app gives a grid built only where it shows (RowCount,ColCount,RowIndex,ColIndex, ARIA's aria-rowcount and the like) go to WebKit'sAXARIARowCountand the like, and the rows'AXIndexand the table'sAXRowCountwherePosInSetandSetSizesay nothing; to ATK'srowcount,colcount,rowindexandcolindexattributes besidexml-rolesgridortreegrid, as Chromium's; and to UI Automation'sGrid,GridItem,TableandTableItempatterns of tables and their cells (uia_grid.go), which count from 0 as the app gave them, else as rows say which of all they are after the header rows above them, else by what is built, found as a client asks; theirRowHeaders are theTablepattern's row headers. A cell'sColumnSpanandRowSpan(node.cellSpan, from the layout'sgridCell, whether or not its row is a grid) areAccessNode.ColSpanandRowSpan(aria-colspan, aria-rowspan): the lengths of itsAXColumnIndexRangeandAXRowIndexRange(placeCells, which puts the next cell after the columns it covers), ATK'scolspanandrowspanattributes, andGridItem'sColumnSpanandRowSpan,Grid.GetItemfinding the cell at each place it covers. -
The connection.
content.goattaches the content to its window throughinternal/surface.Conn, which carries the surface and, as functions, what the content needs of the app (the clipboard, dragging the window, the appearance, the room of a hidden title bar's controls, opening URLs, context menus), souiimports neithermygonor a backend, and an app without native UI links none of it.Window.UpdateandInvalidatecoalesce redraws asked from any goroutine into one frame on the main thread, whichConn.Changedasks the content for, so that it builds anew. Page methods returnerrNoPageor do nothing. -
Frames. The engine (
ui/runtime.go) calls the view to build a frame, again (up to three times) when a handler changed the state while it built, so the frame shows the outcome. A window that closes during a frame (a close button's click; backends reportClosedat once) keeps its engine until the frame ends: the rest of the view builds with a workingContext, no further callback runs, nothing is laid out or painted, and the engine closes as the frame ends. ObservingPressedwhen a pointer press begins also rebuilds before painting, so selection made on press and its focus colors appear together; holding the pointer asks for no further build passes. The engine lays it out with flexbox (layout.go) or as a grid (grid.go, CSS grid's placement and track sizing for fixed, fractional and content-sized tracks, with the items' percentages of their grid areas, as auto while the tracks are sized), laying out again only what changed since the last frame (see below); commits the boxes to the elements' states with the hit list in paint order, the focus order and the labels; paints ascene.Scene; and presents it. The pointer finds what it is over in the hit list, last first (appendHitChain,contains): an element whose visible part, clipped as commit found it (state.vx..), does not hold the point is passed by before its transform is inverted, unless its scroll containers moved or a pose moved it since (state.reposed), so that a move over a large tree costs little;TestHitFilterMatchescompares with every box tested (engine.noHitFilter). Input between frames goes to the states of the last frame's elements. A release that comes with moves no pass read waits in the element's state (releaseLater) for the pass that reads them, which applies it as it ends (forgetInput): the view sees the element still pressed where the pointer let go, and released in the pass after. An element's identity hashes its parent's with its position orKey, so focus, scroll offsets, editors and animations survive rebuilding. Each pass lists the states it looks up (engine.built,nbuiltof them,lookState, less thoseunlookgives back):forgetInputvisits those alone,prunelooks for states to drop only when the pass built fewer than there are, and the formats of native drop targets are gathered only in a pass that made one (noteTarget), so that what a frame costs past building follows what it built rather than every state of a map; ui's tests check the lists each frame (checkFrames,checkBuilt), its benchmarks do not.prunedrops the states the frame did not build (but those of pages Routers keep) and recycles them; a state withLocalvalues that areio.Closers (state.closes, whichLocalfills as it makes one, so that other states cost nothing) hands them toengine.dropped, whichendFramecloses on the main thread once the frame ended, last made first. Those of an element whose copy, or an ancestor's, leaves with anExitwait inengine.exitsuntil the transition's record is gone or no longer holds that copy (the exit ended, or the element came back), and the engine'sclosecloses every state's as the window closes. An element'sOnUnmountkeeps its function in such aLocal, under a key of its own (unmount,ui/local_close.go), whoseCloseruns the function given last.Contextis stable for the window;Elementvalues expire between build passes and validate their owner, slot and generation before accessing storage.ListStateresolves its focus owner only for the current context, frame and pass, exposing focus and shortcuts without retaining an element in app state. Scroll offsets move in the layout too (ui/scroll.go): elements that asked toScrollIntoView, and the focus as the keyboard or the app moved it (node.focus; a click does not, nor the app while a pointer button is held or withFocusOptions.PreventScroll), come into view before the boxes are placed, lined up as its options say (or glide there as a wheel's notches do, by what they went each frame,glideScroll), and placing keeps each offset within its content; aScrollStatemirrors an offset both ways, and a frame that moved one builds another for what read the old one. A container that snaps (ScrollSnap,ui/snap.go) finds the places its elements mark as it is placed, from their boxes relative to it, and keeps the offset at the one it rests at; a notch or a key heads for the next place at once, a touchpad's scrolling once it has been still for a moment (not every platform tells when the fingers lift or a fling's momentum ends), along a spring that the frames advance (glideSnaps). OnlyScrollSnapinstalls what snapping takes (placeSnapper, thesnapperinterface of the container's state), so apps that do not snap leave it out. Frames happen only when asked: input,Invalidate,After, orAnimationFramewhile something moves. The view builds the whole tree each frame, but layout reuses what did not change (ui/layoutcache.go), as Blink reuses the layout result of a box not marked as needing layout in the same constraint space and Flutter skips a render object whose constraints are those of its last layout. Nothing marks a rebuilt tree, so before laying outsameLayoutcompares each element with what its state kept of it from the last frame: itslayoutStyle(the fields ofnodethat lay it out, embedded in it), the flags and inherited text styles the layout reads, its grid's tracks, its spans, its image's size, and its children, the same elements in the same order, each the same in turn.layoutBoxthen reuses the last layout of an element that is the same when it gets a box of the same size: its text's layout and its children's boxes, laying out again any child laid out at another size since; what was measured of it (its content widths and heights) is reused too, so that a container laid out again measures its unchanged children at once. The state of a text element also keeps the widths of its text and its last two layouts (heldText), found again by their parameters, as Taffy keeps a node's measurements: an element laid out anew whose text is the same, as the rows of a List each frame, asks nothing of the text system's cache, whose key costs more to hash than to compare, and one laid out at another width asks for it from the last (text.System.LayoutFrom). A grid measures the widths of only the items its columns read (contributes): none for Fr columns with room known, as CSS's minmax(0, 1fr), unless they areMinContent; a minimum (Track.Min) measures nothing. Transforms and scroll offsets of other containers apply after layout and change nothing there. Elements that lay out by more than they are built with are never reused, nor anything around them: a List (rows built as it lays out), a Form, a Table's fitted columns and a toolbar (which change elements inside them as they lay out, so neither is anything inside those), an input, an element attached to another. A field added tonodemust say whether layout reads it (TestLayoutInputs);TestLayoutCacheMatcheslays random trees out with and without the cache (engine.noLayoutCache) as they change at random, frame after frame. A frame asked for only by drawings that move (Painter.AnimationFrame,Painter.After, as spinners and progress bars of unknown length do) paints the elements of the last frame again at its own time (repaintFrame), without building or laying out; elements out of view are not painted, so they ask for none. Nor are the others, mostly (ui/repaint.go): the frame painting the window keeps, for each Draw or DrawOver callback that asked for frames, the operations it painted and the painter as it was (paintPart), and a frame that only paints again runs those callbacks alone and splices what they paint into the last frame's scene in place of what they painted (repaintParts). It paints the window whole when anything else asked (a pose moving, a drawing painting into a layer or a canvas, asengine.untrackednotes), while the inspector shows or a drag moves, and once the parts have added more glyphs, stops and words thanpartsGrowthpast what the whole frame held.TestRepaintPartsMatchWholecompares the frames with those painted whole (engine.noParts). Such a frame is asked for withredrawset, which every event of the surface clears (buildNext), so that the frame builds anew in case the event changed what the view shows; anything asking for a frame built anew clears it too (askBuild), and the framePainter.Afterhas due with it, which the frame asked for replaces:requestFrame,Conn.Changed(Window.Update,Invalidate, andAfter's timer through them) and a change of the appearance. An event that asks for no frame, as the pointer moving over elements that do not look at it, leaves that frame due. A hover that only changes looks paints the last frame's elements again, whole, rather than building the view (lookDue,askLook), as browsers restyle and repaint for:hoverwithout running a page's scripts: a button the pointer comes over in a window of thousands of elements costs a paint, not the whole view.Hoverednotes who reads it: the view, as it builds (state.hoverBuilt, reset as the pass looks the state up), or painting (hoverLook, reset asPainter.elementpaints it), as the controls' styleFns pick hovered backgrounds and borders and scroll containers show their bars.hoverNeedslooks at the elements that came under the pointer or left it alone (those hovered still see no change): one read as built, showing a tip, with aTransition(whose colors move in frames built anew), or looking at its hover (flagHover) without having told how, builds anew, as do elements following the pointer (flagTrackPointer) anywhere in either chain and a window the inspector shows; one read only as painted paints. A frame built anew asked for since (buildAsked) shows the hover, so none is asked then, and asking for one clearslookDue; a move bringing other modifier keys builds anew, as views may show what they do. The frame that builds a scroll's rows under a still pointer paints their looks already (rehover(false)); after it presents, a hover moved bypruneasks for a frame painting (rehover(true)). A styleFn, and aTransitionmoving colors or shadows, change the element's colors, opacity and shadows as it paints;Painter.elementputs back those it was built with once it painted (builtStyle), so that painting it again starts from them, not from what the last painting chose (a moving shadow points to the transition's own array, never into the built one).TestHoverLookPaintsAsBuiltcompares each frame painted for a hover with the frame built anew, over many controls. What the frame writes after the build into fields the view builds goes in the journal (ui/journal.go): a table's, a form's and a toolbar's fits, a List's offset, an attached element's origin and hiding, the spans a paragraph merges from the texts inside it, the parent of exit copies, and whole elements before a transition, a magic move or the inspector's edits change them (journalKeepNode). The journal goes as the next build begins, its elements built anew; it is what lets an element kept to a later frame without being built again go back to what it was built with (rollback) before that frame lays it out (#1599). What the frame computes in fields of its own needs none: the boxes,world,ink, the measures and layout records, the inline fragments and the tree's links. ui's tests (TestMain) check after each frame that the other fields, the journal rolled back, are what the view built (checkBuilt,builtcheck_test.go, comparing their bytes and the elements of their slices);MYGO_BUILT_COLLECT=1checks after each phase and lists what changed rather than failing, to find the writer. A frame of another size, or after the text system forgot its layouts (text.System.Generation, as it lets go of fonts the elements' layouts hold), builds anew all the same. The timers the last frame built armed stay; the host arms them (afterTimer,repaintTimer), andPainter.After's posts to the main thread. A drawing asking for fewer frames (Painter.AnimationFrameRate) has those frames come every so many of the display's refreshes, the fewest that give it as many frames a second at least (engine.pace), unless anything else in the window asks for each:Painter.AnimationFrame, a pose moving, the view animating. The refreshes between draw nothing (paceSkips) unless something else asked for a frame since, and the surface is told of the rate (platform.FrameRateSurface): macOS sets its display link'spreferredFrameRateRange, which has a ProMotion display refresh as often while nothing else on it asks for more. A surface whose frames asked for come at once rather than at the next refresh (platform.ImmediateFrameSurface: Windows, where presenting paces frames) would ask again and again until the frame is due: the repaint timer asks for it then instead, and the frame it brings keeps the pace's times, as the timer fires a little late. With the display's rate unknown (browsers), frames come at each refresh. While nothing of the window shows (platform.OccludableSurface: a macOS window hidden, minimized or covered by other windows, whose display link still ticks while it is covered, at the display's rate for half a minute, then at about 40 Hz, but not while it is hidden or minimized, where a timer draws the framesRequestFrameasks for instead, a refresh after the last, and the backend's queue (drawSoon) the one after a pause; a minimized window on Linux, as GDK'swindow-state-eventtells on X11, and on Windows, asWM_SIZEdoes, where noWM_PAINTcomes andRequestFramedraws the frame from the queue as well), what moves asks for no frame, and a frame asked for by a change of the state waits (holdFrame; bothheld), as browsers draw nothing for windows out of sight and as Apple asks of Mac apps: the view function,Drawand the workAfterdrives wait until some of the window shows.SurfaceShown, sent as some of the window shows again (windowDidChangeOcclusionState:), draws what changed, and has a held animation go on from where the time puts it; a video (VideoPlayer) goes on from where it stood instead, counting its time from then (engine.shown), as one the view stopped building does. Its frames decode on a goroutine, two ahead, into frames handed back for it to decode into again; the view takes the last frame due, passing over the others, has the bitmap show it (Bitmap.Update), and asks for the next withAfter, or waits for the goroutine to wake the window (waiting) when it has no frame due yet. A video of the system's media framework (ui/video, below) moves on by the framework's clock instead; a muted one holds still out of sight, as a decoder's does. The window shows its last frame until then, about a refresh: captures of it meanwhile, as sharing its window in a video call, see that frame. A frame still builds after input (input, as keys typed into a key window another app's covers), as assistive technology turns on, and forCapturePage, which builds the frame waiting, held or asked for and not come (windowHost.waitsOutOfSight), as the surfaces of macOS draw none for a hidden window, nor for any behind the locked screen, whose display link does not tick (a frame built outside the surface's asks for one to show it,unshown, which builds nothing more); and none waits before the window's first frame reached its surface (windowHost.occluded), which a window made hidden waits for to become ready to show.Context.WindowHiddenreadsoccluded; once a view asked (hiddenAsked),SurfaceHidden, which the surfaces send as none of the window shows any more, builds one frame out of sight, as input does, for the view to stop its work, andSurfaceShownone whether a frame was held or not. The window gaining or losing the keyboard focus draws a frame only when the last frame read it (windowFocus, as a focus ring, a caret, a selection's color or a focused text input's IME state do): a window hiding or showing again draws nothing it would draw the same. So does the window becoming active or inactive (windowActive, which the widgets read throughcontext.emphasizedandcontext.accentwhere they would fill with the accent, andContext.WindowActive), told bysurface.Conn.ActiveChanged. Each frame matters on macOS, where the Metal driver takes 30 to 45 MB of the process's footprint ("owned unmapped (graphics)") for the first command buffer encoding anything after a pause, and gives them back a second or two after the last, as a bare Metal app does. A frame allocates next to nothing once the view builds what it built before: elements come from the context's arena, the default theme is copied for each pass, the states that pruning frees go to new elements (as rows coming into a list's view take those of rows that went out of it), without keeping their local resources, editors or callbacks. The arena uses chunks of 32 elements, releases unused chunks after a smaller frame, and keeps one empty chunk for growth; unused elements and the spare arena of exit transitions give up their references after the frame copied what it needs. A text keeps in its state what it made of its spans (spanCache: their text, the styles of their layout, encoded, and where each ends), which a frame compares rather than makes again. WithMYGO_FRAME_STATSset, frames slower than its threshold log how long each part took, which path drew them, what the process allocated and whether the collector ran (ui/framestats.go). WithMYGO_UNCAPPED=1, whichinternal/pacingreads once (Uncapped), frames wait for no refresh, for benchmarks, profiling and the least latency (#1496): the next frame asked for starts once the last was presented. Only the frames asked for come sooner, so an idle window still draws nothing. The host holds no frame drawn in memory to the refreshes (refreshPresented) and counts every surface's frames as coming at once (framesAtOnce), soPainter.AnimationFrameRatekeeps its pace with the repaint timer; the backends and renderers each skip their own wait: Direct3D's swap chain has no frame latency object, three buffers and, where DXGI allows it, tearing, and presents every frame with sync interval 0, withoutwaitComposedorwaitRefresh(internal/gpu/d3d11/uncapped.go); macOS draws each frame from a timer firing at once (surface.frameSoon), not the display link, into a CAMetalLayer whosedisplaySyncEnabledis off; Linux tells GTK's frame clock a refresh interval of a microsecond (paceFrameClock) and, on X11, sets the GLX swap interval of the window GDK paints to 0 (gl.UnsyncGLXSwaps); browsers keep requestAnimationFrame's pace. On Windows a live resize presents paced, from the window's first change of size until the swap chain settles (presentsUncapped): no tearing, a frame of a new size waiting for DWM to show it and the others for the composition that shows each, as uncapped frames queued for DWM while it resized the window, and one present took 2 s. Each of those waits is bounded, the frame of a new size's at two refreshes, with DwmFlush called on another thread (waitShown): called on the main thread withinSetWindowPos, it was the one wait with no bound when a present took 2 s at the start or end of a resize the app made. Nor does Present wait long then: with no frame latency object to tell when the swap chain has room, Present waited for it on the main thread, and a frame redrawn in part still took 2 s in 2 resizes of 12, most likely queued behind buffers DWM held. While resizing, frames present withDXGI_PRESENT_DO_NOT_WAIT, tried for a refresh at most (showSoon); one Present still refuses waits, unpresented, for the next composition while the main thread takes its messages, or gives way to the next frame drawn (holdFrame,presentHeld). One still not taken aftermaxHeld(100 ms) presents without the flag, waiting as long as DXGI has it, on another goroutine (presentAside,goAside), and until DXGI takes a frame at once with the swap chain settled (gaveUp), frames stay paced with the flag and the buffers resize, to settle or to grow, on another goroutine too (resizeAside). Meanwhile the main thread makes no call to the context nor the swap chain: a frame drawn then is put off, the frame after waits (Ready),Releasewaits for the call (2 s at most), and once it returned the frame after draws, or the window is asked for one (askFrame). A resize's last frame held for good otherwise left nothing drawing; dropping it and drawing on had the settle frame take 994 ms on the main thread, inResizeBuffersor a plain Present; and putting such frames off until DXGI took one with the flag and an event query said the GPU was done, waiting on nothing (0338404), put every frame off for good in 4 resizes of 70: nothing the app did without waiting ended DXGI's refusal, while the call that waited did, so such a call is now made, off the main thread. Uncapped, the refresh period is read from DWM on another goroutine (readPeriodAside). WithMYGO_FRAME_STATStoo, the engine logs how many frames came each second (frameStats.rate), counted anew after a pause or a frame as long (250 ms), and each frame's path says uncapped. -
Video of the system's media framework (
ui/video,ui/engine_video.go,internal/media,scene.Surface). Amedia.Engineplays one video: AVFoundation'sAVPlayerwith anAVPlayerItemVideoOutputon macOS, Media Foundation'sIMFMediaEnginein frame-server mode on Windows, GStreamer'splaybinwith anappsinkon Linux, a video element in browsers, each reached without cgo (purego, COM vtables throughsyscall,syscall/js) and loaded with the first video opened. The engine decodes, plays the sound and keeps the clock;VideoPlayer(videoEngine) asks it, as each frame builds, for the picture due at the frame's time (Engine.Frame) and shows it in its bitmap as ascene.Surface(Bitmap.showPicture), which renderers sample where the decoder left it: Metal wraps aCVPixelBuffer's IOSurface planes with aCVMetalTextureCacheand keeps theCVMetalTextures until the frames in flight that read them completed; Direct3D opens the engine's shared texture (an NT handle, BGRA, which the engine converts into on its own device) once by itsNativeIDand reads it under its keyed mutex (key 0) within the frame; GL uploads NV12 planes, 1.5 bytes a pixel, or in browsers the element, GPU to GPU. NV12 is converted in the shaders' YUV image class (the matrix in the instance's params, the chroma in texture slot 10 or 11), which the CPU renderer samples the same way (raster.sampleNV12).Bitmap.UpdateYUVshows an app's own frames so (YUVImage.surface): NV12's planes as they are, and I420's with its Cb and Cr interleaved into a plane of NV12's chroma that the bitmap keeps, a quarter of the frame's bytes, so that the renderers and their shaders know one layout of planes. The window asks for the next picture withAfterat the engine's interval, or an engine that knows when one comes (GStreamer's appsink, the element'srequestVideoFrameCallback) wakes it (Engine.SetWake), as it does for a change of state. A muted video holds its engine paused while out of sight, under less motion, and once a frame that built the view did not show it (engine.sweepVideos); one with sound plays on. On Windows, arm64's doubles go through two thunks of machine code, assyscallpasses and returns only integers there. -
Visual transforms and motion (
ui/transform.go,ui/motion.go). An element's transform, 2D, or 3D in a 4×4 matrix (mat4) that it flattens into its parent's plane as CSS's flat transform-style does, composes with its ancestors after layout as a projective map (scene.Affine, with G and H in perspective); boxes keep their original layout space. Ascene.Affine's divisor is 1 at its origin, or -1 (Behind) when the origin is behind the viewer though what is drawn is in front: a turn in perspective around an element far from the window's origin puts the window's origin past the turned plane's horizon (#1406). Products and inverses keep the divisor's sign, and the shaders take it from the instance (Transform2.z). Elements andPainter.Transformflatten a 3D transform around its own origin and move it there in 2D. A transform that puts all of its plane behind the viewer (TranslationZas far asPerspective's distance, not turned) flattens to a singular map, which shows nothing;BackfaceHiddentests the normal through the whole 4×4 inverse, perspective included (facesAway, as Chromium'sIsBackFaceVisible).TranslatePercentstays apart from the transform (translatePct):transformAtresolves it from the box as laid out, after the transform and before the pose, as a pose's percents (showPose) and aMotion's (tbox.moved) are. Painting stores the composed transform on scene operations and clips, and committing keeps it on the state for inverse-mapped pointer input, carets, accessibility bounds, and overlays attached to visual bounds. The CPU renderer inverse maps pixels for transformed primitives and clips, and leaves what a projective map puts behind the viewer. GPU builders retain local geometry and send three transform rows with every instance; the vertex shaders project the quad in homogeneous coordinates, so that local coordinates interpolate in perspective, and fragment shaders integrate coverage in device space: from screen derivatives for affine maps, and, in perspective, at the CPU renderer's sample points, mapped back from the pixel's center through the inverse. Nested transformed or rounded clips use cached masks, independent of content moving within them, once they hold content that a transform does more than move (scene.Reshaped), or a clip reaching into their rounded corners (scene.ClipCorners, which the scissor rectangles of outer clips would not cut); hard clips in the drawing's coordinates, and clips around content only moved, evaluate on the GPU. Stationary clips reducible to one rounded rectangle also evaluate on the GPU, even around scaled, rotated or perspective content, as a sidebar against the side of a rounded frame does (#1511). A fractional hard edge can remove whole corners; overlapping antialiased edges, transformed clips and image/gradient masks retain their existing mask path, as do continuous corners on the renderers whose shaders don't draw them (all but Metal's,gpu.Builder.SplitCorners). Backdrop effects map local samples into the frame. With a GPU, every frame draws there, including resting changes to transformed content. Metal retains wide colors under transforms. See Transforms for rendering details. -
Masked clips (
ui/mask.go,scene.ClipShapeOf). A clip (OpPushClip) may carry a gradient paint, whose alpha masks it (MaskLinearGradientand the others, around the element's inset rectangle fromClipInset), or an image, whose alpha masks it over its rectangle (Painter.ClipPath: the path's coverage, rasterized byinternal/veconce for each shape and kept by the engine while paints use it). The GPU builders draw such clips into the cached clip-mask textures, as transformed or rounded clips do (raster.ClipMask), and never evaluate them in the shaders, so no shader changed for them. The CPU renderer multiplies the clip's coverage by the mask (raster.clipMasking). Each op under a mask is masked on its own, rather than through a group layer asOpacityfades (below). A clip moved by a translation moves its gradient with it (scene.MovedClipOp), and damage compares the version of its image. -
Motion (
ui/motion.go): every animated value of an element (AnimateTo,Animate,AnimateWith,Spring,Jump) is achannelby key, with a value, a velocity and the time they hold for, moving along a curve or a spring. Springs solve damped oscillators analytically (solveSpring), so frames however far apart follow one path; a channel samples its path at the frame's time before taking a new target or kind of animation, so position and velocity survive interruptions (a curve's velocity is its easing's slope). A delayed target waits in the channel while the old path goes on.Jumpsets values and derives a velocity from successive ones; presses keep their last moves (dragTrack), in the element's axes and the window's, forDragVelocityandDragEnded, and their button (state.pressButton,PressedButton):Pressed,Draggedand the controls that follow the pointer (sliders,Draggable, value drags) take the primary button's alone (pressedPrimary), a right or middle press only clicking or opening a menu. Keyframe sequences keep one sequence per key with an explicit replay token. All ask for frames only while moving, and end at their targets under reduced motion, but for those whoseNonMotion(ofAnimation,KeyframeOptions,TimelineOptions) says they move nothing, which play on. Poses (ui/pose.go,AnimatePose) are channels sampled as frames commit, into a transform after the element's own (poseM) and a fade; frames that only paint (repaintFrame) move the posed elements of the last frame built on (repose) and set their subtrees' transforms again (updateWorld), so they move without the view running.Draggable(ui/draggable.go) keeps two channels of an offset (dragRec) thatshowPoseadds to the pose's translation: as the pointer drags the element, they hold where the press's move in the window (dragTrack), mapped into the parent's axes through the inverse of its last world transform, takes the offset from where the press caught it, so they are the same in every build pass of a frame; once let go, a spring starts at the release's velocity for whereSnapchooses from the projected throw, and goes on in frames that only paint, the view building once more when it comes to rest. Content transitions (ui/content.go) keep what an element showed, and fade or roll it into what it shows as it paints, counting paintings (engine.paints) rather than frames built, so frames that only paint move them on too. -
Transitions (
ui/transition.go) animate elements FLIP-style, after the layout of each frame and beforeplaceturns boxes into window coordinates: an element given aTransitionkeeps, by its ID, where the layout put it relative to its parent, its size, and what it shows, and goes from what it showed to a new place or size over the transition (parents first, as one resizing lays out its content anew at each size, which then moves itself). Moving with a parent or a scroll offset is no change; resizing the window animates nothing, and reduced motion only fades and colors. Each moving component (position, size, opacity, and aMotion's scale, rotation and blur) is a motionchannel, so a spring keeps its velocity through changes and a delay holds them. A blur goes with the opacity, as it moves nothing, and adds to the element'sBluras Gaussians compose, whichpaintEffectdraws on every renderer. Colors move as elements paint, after theirstyleFn, mixed premultiplied in sRGB, or in Oklab (mixLab) with theOklabof theirAnimation. AMatchmoves an element appearing where another of the same identity showed the frame before (ui/match.go,matchRec, in window coordinates) afterplace(matches.move), laying it out anew at each box on the way, or scaling it (ScaleContent), across parents and scroll offsets, from the colors the other showed (matches.claimnotes them beforeanimateExitsforgets it). WithFly, it blends the other's corners, opacity, text color and transform into its own (decomposed as CSS does, in 3D into a quaternion slerped the short way, whose sign holds at half a turn), and the transform of the elements around the other into that of its own (worldAround, shown with a transform of its own,under). The element it came from goes with it if it flies too, fading out as it fades in (Motion's crossfade curves): itself while still built, as in a page going away, else a copy thatmatches.claimtakes, in place of itsExit, from the arena of the frame before, which frames with an element flying keep as those with anExitdo, with the text style and the theme of the elements around it; copies of its ancestors going away hide theirs. Both arelifted, over the rest of their layer: the window's content, which the root paints them over before the overlays, or the overlay they are in, which its layer paints them over (matches.paint), out of their ancestors' clips and opacity, in the theme of theThemedaround them.commitlists their hits there too (engine.liftHits, put over each layer as the next starts), wherecontainsstops at them the clips around them, so that they take the pointer where they paint. Each is placed where its content sits on the pixels as at rest, and shown where it is with a translation (matches.pin), which elements flying inside it take into account, so that its text moves as one, smoothly, and ends as text at rest; a flight ends with its look, its box within a quarter of a point of its own. One gone with an ancestor going as itsExitsays flies on as its copy in the ancestor's (matches.flyOn), from where it showed to where that copy has it, its colors going on as its transition moved them, fading as that copy fades; other copies of elements flying (ghostOf) no longer fly. A text flying withContentTransition'sMorphtakes the other's text layout into its content transition instead, whosemorph(ui/morph.go) pairs the glyphs it comes from (morphGlyph, the old layout's, colored by the spans its rich text last painted with, whichmorphRestkeeps) with the clusters of its layout (their longest common sequence, then the nearest of the same characters left) and moves, scales and recolors the glyphs of each pair, fading the others, the old text's going with the element's box, and the lines and highlights of both texts' styles and spans. It places each glyph as text at rest where the box goes would (text.System.Origin), drawn at the quarter of an octave nearest the size it shows, and moves it from there with a transform, so that the glyphs move smoothly and together, rather than each a pixel at a time, and end as text at rest. A text sent back to the one it morphs from before it got there morphs back from as far from the end as it was from the start, which shows the same; one that another text takes over morphs from its glyphs as they showed (snapMorph, from the same walk of them,morphPairs.each, as painting them).ContentTransitionlinksmorphonly for aMorph, so that apps without one link none of it. A frame without an element on its way notes only where eachMatchshowed and the element that showed it (matchRec.node), whose look and the transform of the elements around it an element flying from it reads in the next frame, from the arena kept, rather than each frame noting them. The engine has itsmatcheronly once aTransitionhas aMatchorScaleContent, so that apps without them link none of it. An element appears with itsEnteronly in a parent the frame before had; the overlay layer, built only while it holds something, counts as the root there. One no longer built, whose parent still is, goes with itsExitas an inert copy, laid out and painted under its siblings of its z-index without taking room; the frames after one in the overlay layer, or its copy, build the layer, empty but for the copies, which lets the pointer and the focus through (engine.overlayExits). Frames that built elements with anExitleave their arena to the next frame, which builds in the other (Context.spare), so that the next one copies those gone (ghostOf), with states of their own and nothing referring to the app's state. The parent'sChildExitin the frame it goes (context.childExits) takes the place of itsExit. An element appearing withAfterExitswaits unseen (transition.held,flagInvisible) while copies leave under its parent (ghosts, whichanimateExitssets after the elements built have moved), and comes in with itsEnterin the frame they are gone. Siblings are not laid out around a size moving; they move with their own transitions, in step when they share it. -
The inspector (
ui/inspector.go,inspector_details.go,inspector_panel.go,inspector_overlay.go), as Chrome's developer tools. A window whose DevTools are on (surface.Conn.DevTools, fromPageOptions.DevTools) opens it for the Toggle Developer Tools role (Conn.ToggleDevTools), F12, or Alt+Cmd+I (Ctrl+Shift+I), and picks for Shift+Cmd+C (Ctrl+Shift+C). It is a panel built with the view's elements as the last child of the root, keyed, which is narrower by the panel's width (the view sees a narrower window); hits and paint cover the whole window. Its theme is a copy of the default one in Chrome's colors, light or dark, whose accent colors the tree's chosen row. After each frame is laid out, it notes the tree of elements, the keys given them while it is open (rekey), and the details of the one chosen (describe: its styles as CSS rules, own and inherited, its box model, computed values and properties), and asks for another frame when a hash of them changed, which the panel then shows; frames whose elements stay the same draw no more, and the times of frames, which the Performance tab draws against the display's refresh, ask for none. The tree is aListof the opening and closing tags of the nodes shown. Picking takes the pointer over the content before the elements do. Painting highlights the element hovered, or chosen while the tree has the focus, over the content, with a tooltip. The source of the element chosen is the first frame outside package ui on the stack as it is created or keyed (callSite), only for that one. Two elements keyed alike under one parent share one state:rekeyreports it (duplicateKey), logged once and listed as an issue, and in aTesterit panics (engine.strict), without package testing in apps. The Styles tab edits (inspector_edit.go): a view builds its elements anew each frame, so edits are kept by element ID and applied to the frame's elements after the view and before layout (applyEdits), throughsetStyleandunsetStyle, whichownStylesmust read back; a property they don't know stays read-only (styleKnown).describethen overlays what the elements can't show: values as typed, declarations turned off or not understood (overlayEdits). Rules are keyed by their element, as the bar of changes comes above them. Closing the inspector drops the edits.changesAsGo(inspector_gocode.go) writes them as calls of package ui, whose methods a test looks up onElement, from whatsetStyleparsed.:hovforces states by element (inspector.forced):Hovered,Pressed,Focused,FocusVisible,FocusWithinandringShownaskisForced, which the stub ofmygo_noinspectoranswers with false. The tree'sflexandgridbadges show layouts over the content (inspector_layout.go). A grid's tracks exist only as it lays out, sogridLayoutgives them tonoteGridTracks, behind the constantinspectorBuiltsince an empty stub would still grow its frame and code, with the layout cache off while a badge is on for it to run. Laying a grid out again afterplacewould move its baseline items, which are in window coordinates by then.main.go:42opens in the developer's editor (inspector_editor.go):MYGO_EDITOR,VISUALorEDITORnames the command, started in a session of its own so that it outlastsmygo devrestarting the app, and failures go to the Issues tab. Browsers and phones keepos/execout (inspector_editor_none.go). Production builds ofmygo buildleave the inspector out with the build tagmygo_noinspector(inspector_off.gostands in for it, never open), added to the tags ofGOFLAGS, unless-debugorMYGO_INSPECTOR=1. -
Automation.
plugins/automationdrives the app from another process over a Unix domain socket (MYGO_AUTOMATION), gated with the inspector. Core gives it one hook: the plugin'sinitsetssurface.Automateonly when the variable is set, andui'sAttachContent(windowHost.automate, empty undermygo_noinspector) hands it each window'ssurface.Connand asurface.Automated, which reads the last frame's access tree with the elements' keys (node.key, whichrekeysets too), tells whether a frame was asked for and has not come (windowHost.frameAsked), and in a window out of sight builds the frame that waits to build there (Settle,windowHost.waitsOutOfSight), which the surface may draw only once the window shows: the plugin settles before it reads the tree and after it acts or sends input, so that a press reaches the view. Only the plugin callsAutomated's methods through the interface, so apps that don't link it don't link them. Actions and input go toConn.Eventon the main thread, as the backend's events do; menu items are chosen through the backends' test hooks (TestPerformMenuItem,TestActivateMenuItem); captures and scripts throughWindow's andPage's methods.mygo-automation(plugins/automation/cmd/mygo-automation, its code ininternal/automationcliso thatinternal/e2eruns it in process) is a client of the socket on Go's standard library alone: it prints trees as lines with refs (@e7) that a session's file maps to windows and element IDs, gives a number once per session so that a stale ref never names another element, and after--settle(idle, thentree) diffs the elements by ID against what the session saw last. Its guide,SKILL.md, is embedded in the command (skills), so that it matches the version installed. -
Input taken as it comes. An element with
HandleInputgets its input on the main thread as the backend reports it, before the frame (ui/handler.go): keys (with their releases, which ui otherwise ignores), text, compositions and edit commands while it has the focus, unless aShortcutaround it claims the key, and the pointer pressed on it, moving over it or scrolling over it; once it takes a press, the moves and the release come to it, as to a pressed element, and the release comes as the window loses the focus or once a frame builds the element no more (not built again, or built over by an element withoutHandleInput, as when the press moved it into a split), so that the next press starts anew. Those releases, and a press the system takes (platform.PointerCancel: a browser'spointercancel, a pen's or a finger'sWM_POINTERUPwithPOINTER_MESSAGE_FLAG_CANCELED, orWM_CAPTURECHANGEDwhile a button is down, on Windows, whichWM_CANCELMODEbrings too, GDK'sGDK_GRAB_BROKENof the pointer on Linux, a context menu's loop that takes the release on macOS), areInputEvent.Canceled: the handler still gets its release, so that what holds a press lets go, but the engine commits nothing of it (pointerUp'scanceled): no click or right-click, no drop (the value's drag is given up, as with Escape), noDragEnded, no context menu waiting for the release, andPressCanceledreports it, which sendsDraggableback to where the press took it. A cancel coming while nothing is pressed does nothing, so a backend sends it whenever it loses a press, but not as a data drag of its own takes the pointer (OLE's loop,gtk_drag_begin), whose end ends the press. The Linux touchscreen'sGDK_TOUCH_CANCELtaps nothing (touch.go), as its taps press only once the finger lifts. The pointer's events carry their device and pressure (SurfaceEvent.PointerandPressure), which the engine keeps with the buttons they leave down (engine.pointerDevice): a device that tells no pressure presses 0.5 while a button is down and 0 otherwise, as Pointer Events have it, a move that changes the pressure alone is a move, and the element pressed keeps the device and pressure of its press's last move forPressedPointer(state.pressPointer). ItsTextCaretturns on the input method at that caret, with no text around it;InputMethod(false)(TextInputState.NoInputMethod) keeps text on but input methods off, as winit'sset_ime_allowed(false): macOS skipsinterpretKeyEvents:and inserts the event'scharacters, Windows disassociates the IMM context and keepsWM_CHAR, and GTK takes the focus from the IM context and sends the keyval's character; browsers cannot. A composition fn leaves (returns false forInputCompose), the input method gives up on macOS.ui.Shapelays out text without the cache of layouts, for widgets that keep their glyphs, andPainter.Glyphsdraws them where they placed them.ObserveInputhas an element see, without taking it, what comes to the focus while the focus is it or inside it (keys up and down, text, compositions, edit commands), and the presses and releases over it:observeFocusandobservePointer(ui/handler.go) walk the focus or hit chain, outermost first, before anything handles the event, and do nothing while the last commit found no element observing (engine.observers). A text input'sOnTextKeygets the keys that type no text as they come too, afterHandleInput, but first applies the input queued for the editor (widgetTextInput.prepare, as the platform's queries do), so that fn reads the text typed before the key; a key it takes is taken asHandleInput's are. Shortcuts around the focus claim keys first unless the editor wants them (editor.wants), as Enter.HandleTextInputconnects a custom element's application-ownedTextInputClientto the same platform services. A stable, focus-checked adapter supplies absolute UTF-16 text ranges, selection, marked text, mutation callbacks, range geometry and hit testing. It is invalidated on replacement/disposal, and focus changes unmark the old client and reset native composition. Revealing the element, as the focus does, scrolls to the client's caret (clientCaret), not the element's box, which a code editor as high as its lines makes as tall as the file. GTK and IMM32 use bounded surrounding context while macOS can query arbitrary document ranges directly.ShapeTextandShapeRichTextretain per-paragraphTextLayoutgeometry, its lines included (Lines), as a paginating reader needs; an application owns its buffer, selections, rendering, editing and undo policy. -
Sticky elements (
ui/sticky.go).StickysetslayoutStyle.sticky, which keeps the insets of an element in the flow from moving it as relative positioning (relative);place, which reaches an element after its parent and the scroll containers around it, then moves it and what is inside it bystickyShift, as CSS Positioned Layout's sticky positioning does: per axis, the padding box of the nearest container scrolling that way (or following one,ScrollWith), or the window's, less the insets, is where its margin box stays, within its parent's content box (the content it scrolls, for a scroll container), the start's inset winning (stick). Unlike CSS, which makes a container scrolling one way scroll both, each axis looks for its own container, as one here scrolls only the ways it says. Being in window coordinates once placed, the box is where hits, ink, accessibility bounds and the focus ring read it;laidOutOrigin, which overlays attach by beforeplace, andrevealadd the same shift, so that a popover follows it and the focus coming to it where it shows scrolls nothing. It paints in its parent's second pass, among the absolute children, and commit moves its hits there (stickyHits), while it commits in the flow, so that Tab and assistive technology keep its place among its siblings. -
Paint order (
ui/zindex.go).ZIndexsetsnode.zIndexand, on the parent,zOrder, whichaddsets too for a child added with one (a copy leaving); a parent without it paints and commits its children as before, in two passes, with no sorting nor allocation. One with it paints and commits them throughzPaintandzCommit, whichZIndexsets, so that an app never calling it links none of this. It paints them inpaintOrder: the two passes' order sorted stably by z-index, so that equal values keep it, the copies leaving, which keep the z-index their elements had, first among those of theirs, and, of the root, the overlay layer and what follows it last (zPinned), so that overlays stay above the content and keep their order (the overlay layer's own children are never sorted). Commit (commitByZ) commits them in the usual order, which the focus order and the labels follow, noting each child's hits and web views, and then moves them, child by child, to the same order (sorting pointers to the notes, whose code all slices of pointers share), the web views' indexes into the hits with them, so that the pointer and what covers a web view agree with the paint. The order is among siblings alone: a child with a z-index moves with its subtree, inside its parent's clips, and makes no stacking context that would mix it with its parent's siblings. The engine'szScratchholds the buffers, as a stack for the paint's nesting; the scene's order changing makes aCachelayer draw again, as itsscene.Differsees. -
Right to left (
ui/direction.go). Each element has a direction (node.dir): built, it takes its parent's, and the root the window's, whichsurface.Conn.RightToLeftgives (packagemygofrom the app's language), unlessMYGO_DIRECTIONor the inspector's toggle (engine.dirOverride) override it;Element.Directiongives an element its own (dirSet), which those already inside take (setDir), andAttachToandOverlaygive what they build the direction of where it was asked for. The coordinate system is not mirrored, as Godot's: the layout places right to left. Flexbox turns a row's main axis (flipMain, whichReverseturns back) and a column's cross axis (flipCross), asrow-reverseandwrap-reversedo; grids place items from the right of the content, which is as wide as the columns where its width is not known; absolute elements go to the right without insets, andRightwins overLeftthere and for relative positioning, as CSS says. Logical edges (PaddingStart,MarginEnd,Start, ...) write the physical side of the element's direction as they are set, noting which (node.logical), so that a later direction swaps them; logical anchors resolve as they are placed (Anchor.physical). A container scrolling sideways keeps its offset from the start (scrollLeftturns it into the physical one for placing, revealing and the bars), content narrower than it moves to its right, its wheel and keys negate their horizontal delta (scrollBy), and its vertical bar goes on the left (scrollBars). Text getstext.Params.RTL, the direction of paragraphs without a strong character, and lines start at the right (text.Right) whatever they read, while left to right each paragraph starts where it reads from, as before. Widgets read the direction as they build (node.rtl,arrows,state.rtlfor input between frames) to draw and step what is directional, and context menus carryplatform.Menu.RightToLeftto the backends (NSMenu'suserInterfaceLayoutDirection, and the surface view's while the menu shows, as AppKit pops a menu up from its view's top right corner then,TPM_LAYOUTRTL | TPM_RIGHTALIGN,gtk_widget_set_directionand a north-east anchor, the web'sdir). Accessibility needs nothing more: bounds are window coordinates. -
Themes of a subtree (
Themed,ui/context.go). An element given a theme (node.themed) builds its children with it, asToolbarflipsc.buttonsfor its controls:Childrensetsc.themeand restores it, or the window's (c.window, the root's, whichSetThemesets) when the scope began at the window's theme andSetThemewas called inside. Its own text style holds the theme's color, size and family, as the root's does the window's, so that text inside resolves through the ancestors unchanged. What reads the theme after building finds it there too:Painter.Themeis that of the innermost themed element painting (Painter.theme, which canvases inherit), and the commit keeps a scroll container's bar width in its state (barWidth) for the input that hits its bars. Overlays opened in a scope are children of the overlay layer, above the root:Overlaygives those built whilec.themeis not the window's that theme as their own. A copy leaving with an exit transition copies its theme. -
Preferences.
platform.Theme.Preferencesreads the settings of the desktop that controls follow: on macOS,controlAccentColorandNSWorkspace's accessibility display options, with their notifications; on Windows, DWM'sAccentColor,SPI_GETCLIENTAREAANIMATION,SPI_GETHIGHCONTRASTand Accessibility'sTextScaleFactor, withWM_SETTINGCHANGE; on Linux, GTK'sgtk-enable-animationsand the settings portal's accent, contrast and GNOME's text scaling factor (ReadAll,SettingChanged). What the pointing devices can do (Pointer,AnyPointer,NoHover,NoAnyHover, Media Queries 4's interaction features), whose zero is a mouse's, as macOS's always is, comes from the media queries in browsers; on Linux from the sources of the default seat's pointing devices (gdk_seat_get_slaves), followed through itsdevice-addedanddevice-removed; on Windows fromSM_MOUSEPRESENTandSM_DIGITIZER, the touch screen primary in tablet mode (SM_CONVERTIBLESLATEMODE, whoseConvertibleSlateModesetting changes), as Chromium tells them, primary the finest. A change goes throughHandler.ThemeChanged, as the appearance's does; packageuireads them once until the next, the default theme follows them (Theme.follow), andAnimatefollows reduced motion. Packagemygoreads them the same way:Theme.Preferences()keeps them until the next change, with reduced transparency too, which packageuidoes not follow, and without the pointing devices, which pages' own media queries answer. Pages get them asThemePreferences' JSON (itsMarshalJSON, the accent a CSS color ornull), which is mygo-runtime'sThemePreferences:tsgenimports that type for it rather than declaring one (runtimeTypes). A user script after the bridge delivers themygo:preferencesevent at document start; the bridge keeps the latest forpreferences()and sets the--mygo-accentand--mygo-text-scaleCSS variables, the accent left unset where the desktop has none. When they change, the core sends the event to every page and replaces that script (Page.SetUserScripts: WebKit's content controllers remove all their scripts, WebView2 by the ids it reports), so a document loaded later paints with the new values from the start. -
Lists (
ui/list.go) build only the rows in view and keep their place by a row, the anchor, and how far its top is above where the content starts, not by an offset into their content: rows are measured, grow, come and go (found again by key,ListState.Key, within a thousand rows of where they were), and those in view stay put. Building, a list makes the rows its place shows as far as the heights it knows tell, and the row holding the focus and the header it pinned; laying out (layoutList, in place of flexbox), it measures them, places them from the anchor, a request (ScrollTo) or the end it follows, builds the rows still missing, so that no frame shows a gap, and keeps the content's ends at the list's. Heights measured live in blocks of 64 rows, made as rows are measured, and the others are estimated as their average; the content's size, and its offset, are where those heights put the rows, in float64, as all scroll offsets are, so that millions of rows scroll by fractions of a DIP. Following its end with aFollowAnimation, a list notes where the top of its last row was placed; as rows grow or come, that row goes up, and the rows show as far below their place, which a motion channel takes back to nothing (glideToEnd), counted as at the end. Rows are placed relative to the list, the offset they were placed at kept asscrollBasefor placing and revealing. An offset the wheel, the keys, the scroll bar or the app changed moves the place by as much when it is a step (up to two views), so rows not measured yet scroll by the step; a jump goes where the heights known put it once the rows built are measured, and the end to the end. A reveal (the focus,ScrollIntoView) lays a list out anew from the row it shows, the window's ScrollState-set offset of a list built anew is a jump, and each element remembers theListStatethat placed it last. A scroll bar's thumb keeps the content's size of when it was grabbed until it is let go, as rows measured meanwhile change it, and a frame that moved a list whoseVisibleorAtEndthe view read builds another. Rows built as a list lays out handle their input as the view's do, whichlayoutTreethen forgets, asking for a frame if they changed anything, and lays out what they put in the overlay. Rows aScrollToshows at the top go below the header pinned over their section. A list without a size is as high as its rows, as the heights known tell, and one without rows lays out what else was built in it, as a scroll container.Tableis a header of columns over a list, whose rows are as high as their tallest cell, and which takes the focus and the keys for the list. Lists choosing several rows hold them by key in aSelection[K], through the unexported methods of theSelectorinterface, so that the app reads its own keys back typed; the row last chosen (the cursor:Selected, or the list's own), and the row Shift extends from, follow their items by key as the choice does. Typing to choose a row goes through the engine: letters, digits and spaces that no shortcut takes add up in the focused list's state (typed, reset after a second), which the list matches againstListState.Label. -
Menus. Context menus and menu buttons share the machinery of
ui/menu.go: an event asks for an element's menu (askMenu, with whether it is a menu button's), the next frame runs the element's menu function to collect the items, the host shows the system's menu after the frame (popupMenu), and the item chosen comes back to the function in the frame after, matched by place and label. A menu button opens as the primary button goes down on it, taking the release, as AppKit's and GTK's pop-up buttons do, and for Enter, Space and Down. A toolbar's overflow menu asks for an overflowed menu button's menu after the pass built the button (askMenuLater), so the next pass builds it. -
Routers (
ui/router.go). ARouteris a history of entries, each a location (an escaped path and its query) and the pages showing it, one per level of views:View's, then eachRoute.Viewinside a layout's page, made as first built. An entry pushed shares the pages of the layouts whose part of the path it keeps (each entry notes how many parts each of its layouts took), and all of them for another query of the same path. Each view keeps which entry it showed (the router, or the layout page's element for aRoute.View), so a change transitions only the level whose page changed. A view builds the page shown in a column keyed by the page, flaggedflagPage: committing marks each element's state with the page around it (state.page), andprunekeeps the states the frame did not build when a page around them, through nested pages, is one the routers built in the frame kept (engine.kept): the ten pages of the history nearest the one shown, a layout's view keeping those inside the same layout. The focus does not stay in a page kept out of sight. As the page shown changes, the router remembers the element of the page left that had the focus, and moves the focus into the new page, to the element it remembers there, else to the page (flagFocusTarget: focusable, not a stop of Tab), unless an element of the page took it as it was built (AutoFocus), or the focus was outside the router, where it stays and the page's title is announced. A page deeper in the paths slides in over the page left, the one going away built too, inert (flagInert: painted, without hits, focus, labels, accessibility norContextshortcuts), absolutely positioned over the one in flow, both offset by their insets as relative positioning; others fade in alone. WithTransitionMatch, the page going away is built too, inert, fading out under the page coming, absolutely positioned and fading in, while the elements of aMatchmove between them: those of the page going away that flew with the page's show no more (inert, inmatch.go). The first router of a frame takes the back and forward keys for the window, each one also for the focus inside it, which comes first. -
Comboboxes (
ui/combobox.go). A combobox is a text input with a popup of options in the overlay, without a backdrop: the input keeps the focus while the user types and picks, as presses in the popup do not move the focus (flagKeepFocus), and the popup closes as the input loses it. The option the arrows are on is the input's active descendant, which has the focus in the accessibility tree, as a list's chosen row does, and reads as which of how many it is. Fields around an input (a combobox's, a search field's, a token field's) name it with their Label (nameFrom). Backends treat combo boxes as text fields (macOSAXComboBox, ATK combo box with AtkEditableText, UIA ComboBox with Value and ExpandCollapse);AccessSearchmakes a text field AppKit'sAXSearchField. -
Disclosures (
ui/collapsible.go). A collapsible's trigger is aRoleDisclosure, expanded while open: AppKit'sAXDisclosureTriangle, whose value is 1 while open; a toggle button that is expandable, and checked and expanded while open, as GTK's expanders; a UIA Button with ExpandCollapse. Its panel animatesProgresswithAnimate, which follows reduced motion: while it moves, the panel is clipped to that share of the height its content had in the last frame, and its content is built until it has closed. -
Text areas (
ui/textarea.go,ui/textbuffer.go). An editor holds its text as the string of the app's value, which it shares, and where each paragraph starts in runes and in bytes (buffer): the frame compares the value with it at once while it is the same string, and an edit copies the bytes once, where converting the whole text between runes and a string took milliseconds for a few megabytes. Undo keeps the changes of each step (undoStep), not copies of the text; a text the app sets makes the last step one change from the text before it, which undoing takes back, so that a log the app keeps setting holds two texts there, not one a frame. Deleted fragments own their bytes, and discarded history and input events give up their references, so a one-character deletion does not retain an entire old document. The paragraph index is allocated for the newline count up front; scanning ASCII counts runes a word at a time, with the standard UTF-8 decoder for other text. A much smaller replacement releases the large paragraph and height indexes. Grapheme boundaries come from the paragraph of the caret. A text area lays its text out a paragraph at a time (area), as the text system breaks lines anyway, so that the lines are those of the text laid out whole: each paragraph keeps its layout until an edit changes it or the width does, those in view are laid out from the paragraph the view starts in (the anchor) down, those far from view give their layouts up and keep their heights, and layouts own their paragraphs' text rather than keeping old document strings alive. The heights not measured are estimated by those measured. Two Fenwick trees, of the heights measured and of how many are not, give the top of a paragraph and the paragraph at a height in O(log n) whatever the estimate. The area scrolls as a scroll container, its offset the state's (flagScrollY, the content as high as its paragraphs), kept by the anchor as heights above the view are measured; an edit, a move of the caret or a press reveals the caret once. -
Editing layers.
ui/editor.goholds the widget's editing state;editor_history.goowns delta undo,editor_selection.gothe visual range set and caret affinity,editor_navigation.gokeys and pointer gestures, andeditor_layout.golayout and painting.textinput.gobuilds the public string widgets. They use the sameTextInputClientcontract as custom controls througheditor_input.go: native callbacks query and mutate state synchronously, while the widget publishes its bound string during its next build, preserving change propagation in composed controls. Preedit is a virtual document insertion, and its replacement and commit form one delta undo transaction. The paragraph index carries rune, byte and UTF-16 starts; bounded native queries own their bytes so they do not retain old document allocations. Single-line controls retain their current layout themselves, keeping changing input strings out of the system's cache of display text.internal/text/caret.gokeeps both logical edges of bidi boundaries and wrapped lines, and moves between whole graphemes in visual order. Selection gestures produce logical range sets: highlight, copy, replacement and undo use the same ranges, preserving unselected gaps. -
Indexed text storage.
internal/ropeis a persistent AVL tree of bounded, owned UTF-8 chunks, summarized by byte, rune, UTF-16 and newline counts, andrope.Textthe value reading lines, offsets and UTF-16 ranges from it, and lines a batch at a time;ui/text_buffer.goand the code editor's buffers keep their texts in it. Edits copy affected chunks and tree paths; snapshots share other chunks, and export can stream them.TextInputBuffer/TextAreaBufferintextbuffer_input.gobind those roots to the existing editing client. Native mutations publish a new root immediately, using a version check to preserve concurrent program edits. External root changes refresh the widget and clear stale history. The indexed buffer adapter derives line starts from the text tree rather than moving every later paragraph's absolute offsets. Paragraph layout and height caches remain with the widget; newline-count changes update those indexes. Full text is produced for explicit export/value queries, never as a binding update per edit. -
Charts (
ui/chart) use ui's public API alone: scales return fractions, the plot places marks in percentages, and paths turn those into DIPs while drawing. Log and SymLog (nonlinear.go) add only standard-library math: log domains stay on one side of zero, symlog'slog1p/expm1keep a linear region around it. Reversed domains and clamping share the same normalized-coordinate contract. A separately generated D3 fixture checks mappings, inverses, ticks and nice domains; constant log domains intentionally keep one tick even where D3 omits it. -
Code editor and diff viewer (
ui/editor,internal/diff). Packageeditoris built on ui's public API, asui/chartis, with four hooks for it:Painter.TextLinepaints a shaped line with a color per cluster,Painter.Visiblegives the clip to cull rows,KeepFocuskeeps the focus in the editor while its completions and find bar are pressed, andAccessdescribes its text to assistive technology only while it reads the window. Its design follows Zed's:- Buffers (
buffer.go) keep arope.Textand a log of the changes of each version, in bytes and lines, old and new. Editors, the highlighter and the diff follow a buffer by the versions they last saw (sync): selections, folds, diagnostics and caches move through the changes since, with a bias at insertions, so that one buffer is shown by several editors and both sides of a diff. A step of undo keeps the edits that take it back, a list for each version it made, grouped while typing of one kind by one editor goes on within 300 ms, and the selections of the editor that made it. Taking a step back applies its lists from the last, logging a version for each, whose text is its changes' as tree-sitter needs, and keeps the edits taking each back as the step of redo.EditRemotemakes a version outside the history and moves both stacks past it, from the top (rebase): the edit's ranges (span) and a step's lists are of one text, so each moves past the other (rebaseEdits), and the ranges go on down. A step that overlaps them leaves the history, the text staying as it is, so the steps under it move past that step and the ranges together (composeSpans). The cost is a few integers for each edit of the history, whatever the size of the text; a step's replaced text is a string, so undoingSetbuilds the text again where it used to swap versions. - Display (
display.go,view.go). Every line is a row, several when it wraps, none when folded or collapsed by a diff; blocks are rows between lines (a diff's deleted lines, spacers, separators, annotations). A line's top is arithmetic on sparse sums: the extra rows of wrapped lines in a Fenwick tree, and the heights of blocks and hidden lines with their prefix sums; lines are found at a height by bisection. Only the lines in view are laid out and shaped, kept for two frames as Zed's line layout cache is. Wraps are measured with cached advances of characters (monospaced ASCII is arithmetic) near the view first, a frame's budget at a time, and estimated elsewhere. A character shaped alone misses the room the text engine leaves around it in context, as Core Text does between Han and Latin letters and around CJK punctuation in the app's language when it is Chinese or Japanese (System.SetLanguage): laying a line out shapes each row as it is painted, and ends it at an earlier place to break while it is wider than the editor (fitRow), so lines out of view may count a row too few until they show. Closing CJK punctuation starts no row, nor opening punctuation ends one (canBreak). A new generation of the text system measures and shapes everything anew. - Replacements (
replace.go). A range shown as something else is a segment of a line's displayed text, as a tab or an inlay is: its Text, nothing, or for an element an object replacement character the text system gives the element's width (faceRun.obj,Span.ObjectWidth), so that shaping, carets, selections and wraps need no case of their own, lines not laid out yet counting it too. Their elements are built over the text at the places the lines laid out give, a frame painting them elsewhere building again (replacementsMoved). Lines covered through their newline join the hidden spans. Replacements move as selections do, and a change lays out again only the lines of those that differ, so that an app can set them every frame. - Highlighting (
syntax.go). AHighlighterfollows the buffer's edits and gives tokens per line; the default tokenizes lines from the state the line above ended in (lexer.go,lexers.go), keeping the states, and after an edit goes on below it only until a line starts as it did before. A patch's texts start each hunk in state 0, at lines the buffer moves with its edits (setRestarts) and its snapshots carry (Snapshot.Restarts), for aLanguage.Highlighterto start them as well. Optional interfaces give a parser's structure to Select Larger, folding, brackets and indentation, so that tree-sitter can plug in as aLanguage.Highlighterwithout the editor changing. Brackets are matched by scanning lines a batch at a time (rope.Text.EachLine), skipping strings and comments by their tokens, and only lines holding brackets are tokenized for it. - Snippets (
snippet.go).ParseSnippetreads the flat tab-stop and placeholder subset of TextMate/LSP into an immutable template; unsupported constructs return errors. Inserting a template, directly or as aCompletion.Snippet, makes each occurrence of the active stop an ordinary selection. Existing typing, input methods and undo handle them together. A session follows only its next local buffer change, throughapply/sync; external edits end it. Fields move in template order, preserving ownership at adjacent boundaries, and linked deletions stay separate before the editor merges overlapping deletions. Tab ends the typing group and selects the next numbered stop, 0 last; undo, Escape or leaving the fields ends the session. - Diffs (
internal/diff,diffmodel.go). Lines are interned and compared with git's histogram algorithm, falling back to Myers' with xdiff's cost limit, then slid by git's indent heuristic; changed lines are paired by similarity and their words diffed, words one character apart joined (Pierre's word-alt). An edit re-diffs only the region of the hunks it touched, extended to the unchanged lines around. The split view is two editors sharing their scroll, their lines aligned by spacer blocks, unchanged lines too where the sides wrap at unlike widths; the unified view one editor of the new text, with blocks painting the deleted lines from a view of the old one. The split view's sides share its width bySplitPercent, across a divider that moves asui.Split's does. The view lays both editors out in itsDraw, before they paint, to align them, so the build gives the sides factors of growth from nothing (diffSplit), whose widths theDrawworks out with the layout's own arithmetic, to the bit: a width off by a rounding would wrap a line otherwise and build the view again, frame after frame. The sides' minimum is kept against the last frame's width, and a resize builds the view again to keep it at the new one. Unchanged runs collapse into hidden spans under separator blocks. The split view's editors scroll as it is drawn, the unified view's as it is built; a frame whose layout or build moved the scroll builds the view again (rebuildIfMoved,DiffView), as one laying an editor out at another height does (builtViewH), so that what readScrollYorViewportHeightbefore, as an app's minimap, follows. - Parts (
parts.go,diffparts.go). The bars and popups around the text (find, go to line, completions, hovers) and a diff's separators, actions on changes and gutter button are elements the editor places with the behavior they need, as ui's bases are:shieldhands presses on to what is inside them rather than to the editor's handler, andKeepFocuskeeps the typing in the editor. The app's render function fills each, else the exported default, given a handle to the part's state and actions (FindBar,DiffSeparator, ...), whose values the editor reads back after the build, as a query typed. Separators are built by the diff view across both sides, over the room their blocks leave.
- Buffers (
-
Markdown (
ui/markdown), on ui's public API asui/editoris, whose tokenizers highlight its code. The parser,ui/markdown/parser, imports nothing of ui, so thatui/richtextreads Markdown without the view; the view reaches it through its exported methods only (Source,Footnote,Block.RevisionandHealed). It follows cmark-gfm's two phases over flat blocks and inlines with byte offsets: a block parser with a stack of open blocks, line by line, closing those a line does not match before adding a block, so that blocks go in document order; and inlines parsed per block when first asked, with cmark's guards against quadratic inputs (the bottoms of openers, backtick runs, comments and math remembered unclosed). Appending text truncates the blocks from the outermost block open after the last complete line (restart), which CommonMark makes sound: no rule looks ahead, and those rewriting an earlier block touch only open ones; the partial last line never decides what is closed. Definitions made or unmade parse anew the inlines of the blocks that looked their labels up, and the inlines of blocks parsed again are reused when their text is the same. An edit parses from the top-level block before it until a top-level block starts with nothing open (quiet) where one did, and splices the old ones after, shifted. Streaming completes the tail's inlines from the openers the parser left (heal.go) and holds back lines that may still change. The view keeps the document in its root's local state and the spans of each paragraph by block, links as inlineLinkelements, a code block's tokens by line and its text in chunks of 32 lines; it reveals text at a pace, fading it by span colors and element opacity, which only paint, andRevealedreads from the root's state whether the frame showed all of the source with no fade left, a query rather than a callback, as the frames that change it are those the reveal already asks for. ASelectableroot'sCopyDataandSelectedTextsmap paragraphs' runes back to source offsets, to copy text with block separators, Markdown, and the HTML of that Markdown parsed again, whichui/richtextpastes with its formatting, as it reads Markdown with the same parser (ParseMarkdown). Links open in the system only whenui.SafeToOpenallows them, in both. -
Rich text (
ui/richtext). Packagerichtextedits attributed text on ui's public API, asui/editordoes: a focusable box with a text input client,HandleInput,Draw,Accessand its own scrolling, and four hooks of ui for it:ShapeRichTextWithaligns and indents a paragraph's lines,Element.SetChangedreports its edits asChanged,Services.ReadClipboardDataandWriteClipboardDatacarry its formats, andInputSelecttakes a selection assistive technology made.- The text (
text.go,seq.go). ATextis a value: its characters in aninternal/ropetree, and beside it a persistent sequence of its paragraphs (an AVL tree of chunks of 32,seq), each with an identity, a style and its runs: attributes over its runes and, last, its newline's, which an empty paragraph types in, as Word's paragraph mark. Runs are kept maximal. An edit splits and joins both trees, copying the chunks and paths it touches; paragraphs joined or split take the style of the one their first rune comes from, as AppKit'sfixParagraphStyleAttributeInRange:, but for the app'sID, which follows the text and never comes from what is inserted. Lists are flat, an indent per item, as Quill and Automerge keep them, which leaves room for a CRDT. - The document (
document.go,commands.go). ADocumentholds a text, the selection, the typing attributes, and the steps of undo as the texts before and after each, which share all they did not change, with the region each changed and the selections. A log of changed regions by version lets the editor lay out again only the paragraphs a change of the app touched. Typing derives the typing attributes as NSTextView does (the character before the caret, the one after at a paragraph's start, links, code and tags only inside), measured by a Swift probe of NSTextView. - The editor (
view.go,edit.go,input.go,ime.go,paint.go). It lays out a paragraph at a time withShapeRichTextWith, keeping each layout for the runs, style and composition it was made of, and places them by heights in two Fenwick trees from an anchor paragraph, as a text area does: the first in view, or the caret's while the editor is focused and scrolled, which keeps the line being typed still as what is above it grows. Every edit goes through the document, as a step of undo or part of the action's; typing joins typing's step. Attachments are inline objects of the text system; a first line's indent is the layout's (TextShape.Indent), on the side the paragraph starts at, the right of one reading right to left.Document.VisibleParagraphsandDocument.CaretRectlay out what is in view as they are read, as painting does, and the paint that finds the paragraphs or the caret elsewhere than read asks for a frame (Services.Invalidate), asListState.Visiblebuilds another. - Formats out of the process. The clipboard carries the editor's JSON,
HTML (
golang.org/x/net/htmlreads what is pasted) and plain text; browsers carry each text format through the clipboard events. The runs' formats go to assistive technology asAccessNode.TextRuns:AXAttributedStringForRangeon macOS, ATK's run attributes on Linux, and UI Automation's Text pattern on Windows (uia_text.go), whose ranges are COM objects of their own.
- The text (
-
Text selection (
ui/textselection.go).Selectableon a text selects that paragraph; on a container it gives its text descendants one selection. The window keeps endpoints as stable element IDs and rune offsets, and each frame collects participating paragraphs in build order. After layout the shared range is projected onto their editors, whose layouts paint the highlights. Pointer gestures, keyboard extension, native editing commands and context menus use the same endpoints. Nested containers have independent scopes; controls andUnselectablesubtrees do not participate. A drag near a scroll edge asks for frames until scrolling stops. Inline children contribute to their paragraph once, and preparation waits for their final text so a rebuild preserves the selection. A paragraph whose text only grew at its end keeps the selection, as a reply streaming in. A container'sCopyDatamakes what Copy writes from the partsSelectedTextsreturns: each paragraph's ID, text and runes selected. -
Tables (
ui/table.go,ui/editable.go). A table's rows are aList's that scrolls both ways: the list lays its rows out at least as wide as the columns ask (rowMinW), and the header, outside the list, follows its horizontal offset as it is placed (followX). The order and the widths the user gives the columns live in theListState(TableLayout), by column ID. Dragging a header moves its column once the pointer went a few DIPs, past a neighbor whose middle it passes; a double click on a header's edge fits the column as the table lays out, from the intrinsic widths of the cells the frame built (tableFit). Sorting is the app's: the table setsListState.Sort, orSortByfor several columns (a new slice each time, whichsortClickedmakes), and shows it, to assistive technology as well (AccessSortAscendingandAccessSortDescending: AppKit'sAXSortDirection, ATK'ssortattribute, UIA's ItemStatus). AnEditableTextin a row reads the row being built (Context.row): the list, which keeps the focus, asks for the chosen row's text to be edited on Return (macOS) or F2, which it takes only once a row has such a text; a click on the text of a row chosen before the click edits it once the double-click time has passed. -
Outlines (
ui/outline.go). An outline flattens the open part of its tree into rows every frame (OutlineState.flatten), which aListor aTablebuilds as they show, keyed by their items. Rows are tree items, with how deep they are (AccessNode.Level) and whether they have children (AccessExpandable), open or not: on macOS, the rows of anAXOutlinewithAXDisclosureLevelandAXDisclosing, which keeps its rows as a table does; on Linux, thelevelattribute and the expandable state; on Windows, Level, and ExpandCollapse with LeafNode for items without children.OutlineState.Prefixbuilds the start of the rows in place of the indentation and the arrow (prefix), withOutlineRow.Arrowthe arrow's click and Option-click. -
Trees (
ui/tree.go).TreeBasebuilds every item that shows, in the order the app builds them, eachGroupa column keyed by its item so the items inside keep their IDs as items above open and close; theTreewidget isTreeBasewith its look. The tree is aFocusGroup, one stop of Tab, with the items taking the focus themselves. An item's keys open and close it as it is built; those moving the focus, and the first letters typed (flagTypeSelect, carried over to the item they move to), apply once the view is built (treeBuild.finish, fromcontext.onBuilt), against this pass's items in order, which also says to assistive technology which of how many each is, as an outline's rows. -
Drag and drop within a window (
ui/dragdrop.go). An element dragging a value notes it in its state (Drag), or a function giving it as a drag starts, as a list's rows chosen. The engine starts the drag once the pointer pressing it moves a few DIPs (dragMove), and follows the innermost element under the pointer whoseacceptstakes the value, whichDropandDragOverset from their type. Releasing drops the value on it and is no click (dragEnd); Escape gives up. Each frame scrolls the scroll container under the pointer near its edges, and paints the source's element of the frame again under the pointer, shifting it there and back, with a count beside the pointer for several rows. Lists and grids take their own rows (rowDrag,itemDrag), placing them by the middles of the rows of the last frame. -
Native data drags (
transfer,drag.go,ui/data_drag.go). An element withDragDatapromotes the gesture to the native drag tracker.transfer.Dataholds immutable items with MIME-tagged representations; providers are advertised lazily and cached per session.DropDataaccepts matching formats with a negotiated copy/move effect. The core owns one live source, identified by a random token: backends transfer the token as data, never a Go pointer. Only the core resolves it to the original items and optional typed value, while the source lives. A completion or cancellation invalidates the token and calls Done once.Surface.StartDataDrag,CancelDataDragandSetDropFormatstranslate to AppKit sessions/pasteboard item providers, GTK selections, or OLE IDataObject/IDropSource/IDropTarget. GTK collects accepted formats through asynchronous callbacks after drop; OLE runs its native modal tracker after the source input callback returns. None waits on the main thread for data that needs that thread. Dropped bytes outlive native objects; unhandled file transfers still reach the existing file-drop path, with copy semantics. The unsupported backend cannot create windows/surfaces, and continues to fail at Init without linking any native drag code. -
Grid views (
ui/gridview.go). A grid view is aListof rows of items, whose columns it takes from the width its rows had in the last frame: the layout asks for another frame when the width calls for others (gridFit). The list's own state holds the rows' place; the grid chooses items itself, takes the keys, and tells assistive technology about its items rather than its rows, which are RoleNone: a list of all its items, the item chosen its active descendant. -
Sidebars (
ui/sidebar.go). A sidebar is one element taking the focus, whose items note their IDs as they are built: the keys choose among those of the last frame, and the item chosen is the sidebar's active descendant, with the focus for assistive technology, as a combobox's option. Sections and items are tree items, the sections' RoleNone containers leaving them all in the sidebar's tree, whose rows macOS keeps as an outline's. -
Feedback (
ui/feedback.go,ui/toast.go). An alert dialog is a modal overlay as a dialog's, which a click outside leaves, as AppKit's alerts beep: an alert role (AppKit'sAXApplicationAlertDialog, ATK's alert, UIA's dialog pane) named by its title and described by its message. A find bar is keyed, for its state, which focuses its field as it opens, to go as it closes. A checkbox group finds its check boxes as they register with it (Context.checks), then applies a click to them all. A text input notes the modifiers of the Enter submitting it (submitMods), as the editor takes Shift+Enter: the find bar goes back with it. A toast keeps the time it showed (spent) and since when it runs (at): its time stops while the pointer rests on the toasts or the keyboard focus is in them with the window's (toastsPaused), and runs on while the window is in the background: a toast whose time is over by then closes on the next frame, before the toasts build. -
Dates, times and colors (
ui/date.go,ui/timeinput.go,ui/colorpicker.go).DateInputandCalendarshare one month's grid, whose day under the keys is its active descendant. A time input's hours and minutes are spin buttons named after the field, typed into through type-to-choose (flagTypeSelect). A color picker keeps its color as hue, saturation, value and alpha, as a color of red, green and blue loses the hue of its grays, and takes the app's color anew when the app sets another. A color well is AppKit'sAXColorWell, a button on Linux and Windows, its value the color in hex. -
Indicators (
ui/indicators.go). Meters and steppers have a value in a range, as sliders and progress bars do (AccessRole.Ranged): a meter is AppKit'sAXLevelIndicator, ATK's level bar and UIA's ProgressBar; a stepper AppKit'sAXIncrementor, ATK's spin button and UIA's Spinner, taking increments. A range slider's knobs are sliders of their own, named after it (nameJoin). -
Forms (
ui/form.go). A form notes the label boxes of its fields as they are built, and makes them as wide as the widest before the layout first measures or lays it out (intrinsicandboxLayoutcallalignLabels), its fieldsets' fields too. The row of a field in a form lines up the first baselines of its label and control after laying them out (alignBaselines), from the text layouts of the frame; a control without text centers on the label's line. Fields name their control with their label (nameFrom).DescriptionandErrorreach assistive technology asAccessNode.DescriptionandAccessInvalid:AXHelpandAXInvalidon macOS (the newer API has no invalid state, so the element answers it throughaccessibilityAttributeValue:), the ATK description andinvalid-entryon Linux, UIA's FullDescription, HelpText (unless a placeholder takes it) and IsDataValidForForm on Windows. Tooltips describe their element as AppKit's and GTK's do. Committing a frame marks disabled the state of what is inside a disabled element, which a fieldset disables after building it: input in the next frame then finds it disabled (Element.disabled). -
Focus groups (
ui/focusgroup.go). Committing a frame notes the outermost focus group (FocusGroup) each element of the focus order is in, beside its dialog. Tab skips the elements of a group but its entry, the one that had the focus last, else the radio button or tab chosen, else the first, and leaves the group; the arrows, Home and End move the focus among its elements when no element around the focus takes them as a shortcut, and click the element they reach in a radio group.Toolbarlays out (layoutToolbar) the controls that fit, takes the others out of the flow (absolute and invisible,collapsed), and lists them, by label, for its overflow menu, whose choices click them after the pass (clickLater). -
Overlays (
ui/scope.go) scope the keyboard. Committing a frame notes the dialog each focusable element is in (an element madeModal, asDialogBase's backdrop,flagModal; a popover is in its anchor's), the dialog on top (engine.modal, andmodalLayer, the element at the top of the overlay holding it), and moves a popover's elements after its anchor in the focus order. Tab cycles the dialog on top, the focus moves into it while it is outside, the window's shortcuts built outside it do not fire, and the accessibility tree is the dialog's layer and what is above it. Overlays register Escape as overlay shortcuts (OverlayShortcut), which the keys the focus and the elements around it leave reach, the last registered (the overlay on top) first. An element takes one key a frame (routeKeys): from a second key for it, the keys wait for the next frame, which the first changed, as a Down and an Enter typed quickly into a menu, the Enter choosing the item the Down highlighted. Each element built at the top ofOverlaynotes the focus as it opens (openers), which pruning gives back once it is gone with the focus that was in it. An element attached to another (AttachTo, whichPopoverBaseand the popups of selects and comboboxes use) is that element's popover (Element.popover, andstate.anchoronce committed): its layout (attachTo) finds where the target will be in the window from the boxes laid out before it, the scroll offsets around it, and for an inline target the paragraph's layout (laidOutBox), so that a popover follows its anchor in the frame that moves it; then it goes to the other side, or the other way along the target, where that leaves the window less, and moves along the target into the window (alongTarget); one that scrolls vertically is first cut to the room the window has (roomAlong), as a select's long list of options. Popovers have no backdrop: the engine notes what each press went down on (downs, for a pass), andPressedOutsidewalks from the last through parents, and from a popover to its anchor, which keeps presses in popovers of elements inside the panel, and on its anchor, inside it, as the press goes on to what is under it; a press on the anchor after one elsewhere in the same pass, as on a web view's page, leaves open the popover it opens, and an element the frame first builds was pressed outside of by none, as the press opening it is still indownswhen a quick click opens it. A select's popup keeps a backdrop taking the presses outside, as the system's pop-up menus do. -
Web views (
webview.go,ui/webview.go,webview.goof each backend).Window.NewWebViewmakes a web view of a window showingContent, whose page lives in aWindowof its own that no list holds, withhostset: calls, channels, events,Eval, custom schemes, downloads and permissions go through the code of windows' pages unchanged,CallerWindowreturns the host,CallerPagethe page, and the bridge has the host's id, so the window controls of the page act on it. Its native side (webViewNative) takes page methods from the backend'splatform.WebViewand window methods from the host's, which drag regions use.ui.WebViewplaces it: committing a frame notes where each web view's element shows (noteWebView: its content box on whole pixels, clipped as its hits are, with the hole's corners and, under a transform that only moves and scales, the size the page lays out at,WebViewPlacement.WidthandHeight, whichFrameshows scaled; under another transform, the bounds, upright), and painting it makes a hole (OpHole) through what the window painted, rounded as the border's inner edge and cut by the clips around it, so that what paints after the element paints over the page. The native web view is under the surface, which shows it through its transparent frames; what shows over it blends with it as over any pixels. After presenting a frame, the engine gives the surface each web view's frame and clip and the boxes of the hits after its own within its clip, as they show (Covers): the pointer is the page's where it shows, inside its corners (Radii), and nothing painted over it takes the pointer (WebViewPlacement.At), as a dialog's backdrop does over all of it. Backends cut the page to the corners and show it scaled, keeping its layout, so that it shows as its hole does over a transparent window too. A press there goes to the page and comes to the content asWebViewPress, which notes it forPressedOutsideand moves the focus to the web view's element, unshown; the engine sets no cursor while the pointer is over a web view (cursorUnknown), whose page sets it. Web views the frame does not build hide and keep their pages. Glyphs over a hole are not on an opaque background (Painter.holes), so that subpixel ones do not color the page. Below full opacity a hole takes only that much of what is under it away (OpHole'sOpacity), so the page blends over it. The element is a Tab stop: focusing it gives the page the keyboard (WebView.TabInto, from its end going back), and Tab past the page's ends comes back asWebViewTabOut, from which the focus moves on. A key the page leaves unhandled comes asWebViewKey: the engine moves the focus to the web view's element, if it was elsewhere, and takes the key as from the keyboard (webViewKey), so that the shortcuts around the element and the window's see it; Tab never comes. The access tree has a node for each page, inside its element (AccessNode.WebView), which the backends fill with the page's own accessibility, as SwiftUI'sWebViewis in place. Backends:- macOS: the WKWebView is in a flipped clip view (
MyGoClipView, its layer masking to its bounds) placed at the clip, under the surface in the content view, in paint order, in a pane (another) at its frame, whose bounds are the page's size: AppKit draws it scaled and finds the pointer in it as in any view of scaled bounds, and the pane's layer is cut to the corners (cornerRadius, continuous, andmaskedCorners; aCAShapeLayermask for corners of different radii), in the layer's units and orientation, whichconvertPointToLayer:tells, whatever AppKit does to scale it. The Metal layer is not opaque, and frames drawn in memory have their alpha. The surface'shitTest:returns nil where a web view takes the pointer, so AppKit finds the page under it. A web view's tracking areas get the mouse's moves wherever its frame is:MyGoWebViewdropsmouseMoved:,mouseEntered:andcursorUpdate:where the content painted over it, and as the pointer goes there from the page sends WebKit an exit outside the view, which WebKit takes as a move there, unhovering the page; WebKit sets its cursor only wherehitTest:finds the web view, and the surface sets none over a page.TabIntoputs the web view after (before) the surface in the window's key view loop and selects it, so WebKit focuses the page's first (last) element; as Tab leaves the page WebKit selects the next key view, whichMyGoWindowturns intoWebViewTabOut. The page's node lists the WKWebView, whoseaccessibilityParentis that node; the clip view lists none. WithScrollToContainer,MyGoWebView'sscrollWheel:gives the event to the surface's conversion (scrollWheel) instead of WebKit. WebKit sends a key the page did not handle again, and the WKWebView on up the responder chain:MyGoClipView'skeyDown:sends it to the surface asWebViewKey(unhandledKey), Tab aside. An Escape (or Cmd+.) comes ascancelOperation:first: WebKit sends a command its web process does not know up the chain and resends the key only if no responder took the command, and theNSPanelof a flyout takes it, soMyGoClipViewtakescancelOperation:itself and sendsWebViewKeywith Escape (unhandledCommand). - Linux: the first web view puts the surface's area in a
GtkOverlay, over aGtkLayoutholding the WebKitWebViews (which, unlike aGtkFixed, asks for no size), as an overlay, which GTK realizes again with a new GL context (the GL renderer makes its objects again). The GtkGLArea has an alpha channel from then on, and frames drawn in memory paint over the web views (CAIRO_OPERATOR_OVER). An input shape on the overlay's window of the area (gdk_window_input_shape_combine_region) lets the pointer through to the pages, whose windows then set the cursor; a web view's ownbutton-press-eventtells the content. WebKitGTK focuses no element as a view takes the focus, soTabIntoblurs the page's and passes the Tab being handled on to the view (gtk_widget_event); WebKit moves the focus out as Tab leaves the page through GTK'sfocussignal, which the view's handler turns intoWebViewTabOut. The page's node lists the view's accessible, anAtkSocketthe web process's tree is embedded in; the overlay is a subclass whose accessible lists the surface's alone. WithScrollToContainer, a handler of the view'sscroll-event, which runs before WebKit's class handler, converts the event as the area's does (scrollEvent, moved by the view's allocation), sends it to the content and stops the signal. A scaled page's view is as large as it shows, and WebKit zooms it by the scale over the page's own zoom (pageScale), so that it lays out at the same width in CSS pixels. GTK runs each handler ofdrawwith a clip of its own, and GDK draws WebKit's frames with OpenGL within a clip's rectangles alone: the view's handler ofdrawclips to the page's corners on whole pixels and calls WebKit's class handler itself (GtkWidgetClass.draw), and the region shaping the area keeps the pixels the corners cut off. WebKitGTK dispatches a key the page did not handle again once its web process says so, and its class handler ofkey-press-eventthen passes it on: a handler connected after it (cbWebViewKey) gets those keys alone and sends them asWebViewKey, but Tab, modifier keys on their own and the shortcuts of menu items with a role, which go on to the menu (cbMenuKey). - Windows: WebView2 in visual hosting, a composition controller
(
ICoreWebView2Environment3) whose visual is in a DirectComposition target of the surface's window that is not topmost (a window has one of each), on a device of its own without a rendering device (DCompositionCreateDevice2): the surface's frames, in the topmost target, are over it. The surface composes its frames from its first web view on, as in a window without a redirection bitmap: it sendsSurfaceRenew, and the content makes its renderer again for a swap chain for composition, or shows frames drawn in memory through the compositor; GDI would draw under the web views. Each web view's visual is in one clipped to its clip, the page at its bounds in the surface's pixels, cut to its corners by a rectangle clip (IDCompositionRectangleClip, its setters taking floats through purego) and, scaled, at the page's size, which the visual's transform scales to its frame; the mouse goes to the page through the inverse. The surface gets the mouse everywhere and sends it on (SendMouseInput) where a web view takes the pointer, or to the one a press went to until the buttons are let go (with the capture), with a leave as the pointer goes; it sets the cursor the page asks for (CursorChanged), and a press moves the focus into the page.TabIntoisMoveFocus(next or previous), andMoveFocusRequestedbecomesWebViewTabOut. The surface's drop target sends drags over a page to its composition controller (ICoreWebView2CompositionController3'sDragEnterand the others). WithScrollToContainer,forwardMousekeeps the wheel over that web view (WM_MOUSEWHEEL,WM_MOUSEHWHEEL) for the surface, which scrolls the content as over its own elements; a touchpad's pinch, the wheel by less than a notch with Ctrl held, still goes to the page, as pinches do on the other platforms. WebView2 tells of no key a page leaves unhandled:keys.js, built frompackages/bridge/src/keys-world.ts, runs in an isolated world of each document of the web view, frames in its process included (Page.addScriptToEvaluateOnNewDocumentwith aworldName), decides after the page's handlers ran with the rules ofkeys.ts, and calls a binding of that world alone (Runtime.addBindingwith anexecutionContextName, afterRuntime.enable), which WebView2 raises asRuntime.bindingCalled(GetDevToolsProtocolEventReceiver);bindingCalledsendsWebViewKey. The page's scripts reach neither the world nor the binding, bridge or not, and their events are not trusted. For UI Automation the root also answersIRawElementProviderHwndOverride: for the window WebView2 shows a page with, it returns the page's node, which answers with WebView2's provider of the page (get_AutomationProvider), as WinUI 3 does. That provider has no children: the page's elements are under a top-level window of WebView2's (Chrome_WidgetWin_1), whichGetAutomationProviderForWindowdoes not map to it, so UI Automation finds them there (seen with a runtime of October 2026).
- macOS: the WKWebView is in a flipped clip view (
-
Context menus (
ui/menu.go) open in two frames. A right-click or the menu key marks the element, from the states of the last frame, and the next frame runs itsContextMenufunction to collect aplatform.Menu; after the frame, package mygo shows it withMenu.popup, posted to the main loop, since native menus wait in a loop of their own. A choice comes back as the item's place and label, and its ID when the view gave it one (MenuItem.ID,menuRun.keys), which the next frame's run of the function matches to reportChosen, by the ID alone when there is one; text inputs' menus queue their editing commands instead.PopoverMenuopens the same way and draws the menu in the window instead (ui/popovermenu.go): the frame that would show it notes it (menuState.drawn, whosemenuShownholds where it opened, from the element's corner, and the submenus open with their highlights), and while it shows, each frame runs the function to collect theplatform.Menuand builds the menu from it with the parts ofMenuBase(menuParts), which hold the keyboard and focus model of every drawn menu: themed, and headless forMenuBaseandContextMenuBase, whosemenuShownis a local of their trigger and whose open state is the app's*bool.Popupbuilds an overlay of a point attached to the element where the menu opened, so that the menu flips about that point at the window's edges, and a panel (RoleMenu) for the menu and each submenu open, each attached to the item opening it (the submenus thatSubmenuregisters, built after their menu's panel, as its siblings). A panel keeps the focus for its items, which the keys and the pointer highlight (ActiveDescendant), by their element IDs; the keys are read once the panel's items are built, which they move among, and Enter clicks the item highlighted for the next pass (clickLater), which reports it asClicked; an item clicked closes the menu once its panel is built (finish), unless the view marked itKeepOpenafter making it. AGroup's items are a range of its panel's, which the keys take as one stop (menuStops: the item checked, else the first), Left and Right moving within it. The menu closes for presses outside it but in the frame it opened, whose press opened it. An item chosen runs the function again at once, as the choice from a menu of the system's does in the next frame, or does its editing command; the overlay going gives the focus back to the element that had it. A menu whose element a pass does not build closes (resolveMenu).PopoverMenuButtondraws a menu button's menu so (menuShown.below): opened below the button itself (menuState.below, whichopenMenuButtonsets), its panel attaches to the button's bottom, flipping above it, and the button shows expanded; opened from a toolbar's overflow menu, it goes at the overflow button's corner as a context menu goes at its point. Opened by the keys or assistive technology (menuState.key), it starts on its first item, and a press on the button while it shows closes it (buildMenu, andnewMenufor the bases). A window whose view setsPopoverMenus(ui/menupresentation.go,engine.menuPresentation) draws every menu so:ContextMenuandMenutakePopoverMenu's andPopoverMenuButton's path, and so do the widgets built on them. The drawn path is reached only through function valuesSetMenuPresentationsets (menusInWindow), so apps that never draw their menus do not link it. A text input's or selectable text's own editing menu has no function of the view's to build it.resolveMenunotes it as a drawn menu (drawnMenu.edit) and asks for another pass. After that pass's view, before the overlay layer joins the root, the frame finds the text's element in the content or the overlays (node.find) and draws the menu of itsEditItemsthere, in the element's theme. One in a part that takes no input (flagInert), as a page going away, is not drawn, which closes it. The inspector's fields keep the system's: its panel is built after the overlays, over them. TheTesterreads a drawn menu's items as last built (drawnMenu.items) and chooses one by its ID (drawnMenu.choose), which the next build takes as a click, so its menu helpers work on either. What a drawn item shows beyond itsplatform.MenuItem(Danger,Trailing,Content) stays on theMenuItemof the run collecting it (menuRun.items), whichdrawnItemreads, so that the system's menus and their backends never see it. An item with an ID keys its row (menuItemKey), whose highlight then follows it as items around it come and go. Once a pass built the menu, the items its levels highlight (drawnMenu.highlighted, by ID or else place and label) go to the next run of the function, whoseMenuItem.Highlightedreports them; when they changed, the pass asks for another, so the function sees the highlight it built.Menu.MaxWidth(menuRun.widths, byplatform.Menu) caps a panel's width, its rows' labels shrinking to an ellipsis. -
Scenes are flat lists of operations in device pixels: rounded rectangles with borders of a width per side, solid or dashed, filled with a color, a linear gradient mixed in sRGB or Oklab, a gradient of stops, linear, conic or radial (
Scene.Stops, which ops point into, at most 32 each), or stripes, the border painted with the op's gradient where it says so (BorderPaint); shadows (blurred rounded rectangles, cut by the box casting them, as CSS's box-shadow is, or inner ones, inside the box where the hole they leave, blurred, does not cover it, as box-shadow: inset is); runs of glyphs, whose masks may take a gradient (paths drawn with one); images, in color or gray, blurred or not (an image'sBlur: averaged over squares and blurred along rows and columns as a backdrop is, but transparent beyond its source rectangle, whichui'sElement.Blurdraws its layer with); effects (OpEffect); holes (OpHole), rounded rectangles made transparent, what was painted there gone, for web views; and pushed and popped clips. Renderers draw the whole scene each frame and retain only textures. Wavy underlines are stroked paths. -
Effects (
scene.Effect) are drawings that packages outside the renderers define, as the official plugins do (the glass plugin's Liquid Glass): a fragment shader for each GPU renderer, in Metal Shading Language, HLSL and GLSL, which the renderer puts between a head and a tail of its own (effect.metal,effect.hlsl,effect.glsl, after its shader's common part, withEFFECTdefined:EffectSource), and a twin for the CPU renderer (EffectPixels), which must draw the same pixels. An effect paints its op's shape from five float4s of parameters (EffectOp.Params, which travel in the instance's slots fills use for colors and gradients), and from its backdrop when it reads one: what the operations before it painted, the area its blur reaches (scene.BackdropOf), averaged over squares of 1, 2, 4 or 8 pixels a side so that the blur stays a few texels wide, then blurred by a Gaussian along rows and along columns, each step kept in 8 bits a channel, as textures hold it, which the shaders sample bilinearly (sampleBackdrop,BackdropImage.Sample). The renderer multiplies an effect's color by its shape's coverage, the clip and the opacity. Effects carry the code compiled ahead of time from what their renderer makes of their shader, with its SHA-256: renderers compile the source when it no longer matches, as when their head or tail changed since, and an effect's tests fail then. Packageuigives packages a hook,Element.Material(aui.Material, built with its element when it is aMaterialBuilder), andPainter.Effect, which only this module's packages can call, since effects are internal types. An effect's shape has continuous corners where its op does, which the renderer covers as such, while the effect gets positive radii and takes them as circular. -
Shaders (
ui/shader) are effects an app writes, in Go's syntax:internal/shader(Ebitengine's Kage compiler, forked, with uint and loops that need no constant bounds) compiles one to the IR ofinternal/shaderir, whose backends write it as a function for the renderer's code to call (Fragmentofmsl,hlslandglsl; only the platform's renderer's backend is linked), and whose interpreter (interp) is the CPU twin. A shader's effect (Effect.Shader) tells where the words of its uniforms go in the buffer its code reads: Metal's struct (buffer 2), HLSL's cbuffer (b2) or a std140 uniform block (binding point 0); the op carries the words and up to four images, bitmaps or canvases (EffectOp.Uniforms,Images), bound to textures 5 to 8 (t6 to t9 in Direct3D). GLSL and HLSL read them as globals, so a shader goes between the effects' head and tail; Metal has no globals, so its shaders are whole libraries with a tail of their own (ShaderSource,shader_ps), whichui/shaderbuilds, so that apps drawing none do not carry it.uiitself only hasPainter.ShaderEffectandPainter.ShaderFill: the compiler, its package variables and go/parser stay out of apps that draw no shader. A shader filling text, paths and icons (ShaderFill) turns the glyph ops its function painted, with grayscale masks, into effect ops whoseEffectOp.Glyphspoint at their mask glyphs (glyphs of color stay an op of their own): the builder makes one instance per glyph, asOpGlyphsdoes, with 1 inParams[1]and the corner of the box the shader fills inGrad, and the shaders' tails cover it with the mask atlas andtextCoverage(above#ifndef EFFECT, as effects share it), the shader's color in place of the paint;raster.maskedEffectis the CPU twin. An element's effect (Element.Effect, aui.Effect) renders the element's subtree into a canvas kept in its state, at the window's scale, in the element's own space (paintEffect: positions shift, and transforms in the subtree lose the element's own,unworld), then paints the effect with the element's transform. The canvas is made in steps of 64 pixels (effectLayerStep), the content in its top left corner, and kept while the content fits and leaves less than two steps over, resized in place otherwise (Image.Resize): an element whose edges cross pixels as it moves, or whose text grows, keeps one canvas and one texture, rather than one a size. The effect gets the content's part (Canvas.region), so shaders read beyond it as transparent, as beyond a canvas of its size; what the subtree paints is clipped to that part, and backdrops end at its edge (Scene.Drawn,Scene.BackdropOf, which the painter, the builders and the CPU renderer use alike), as at a canvas's. The canvas is transient (Image.Transient), unless the element is kept (Element.Cache,ui/cache.go): then it paints the subtree into the other of two scenes and compares it with the last through ascene.Differof its own (layerCache), and only a scene that differs, or is of another scale or part, bumps the canvas's version and goes in the frame'sCanvases; one that does not stays the canvas's scene all the same, as the atlases hold its glyphs this frame. The window'sDifferthen finds the image op the same, so damage over the element redraws one image. A kept element alone (no effect, no blur) renders, rather than its box, what it paints around it (keptLayer.area: each one's box and shadows grown by 4, in its space, clipped by those that clip). Its code is reached only through the interfaceCachestores (layerKeeper), so apps that never call it, nor fade a group (below), do not link it.Opacitybelow 1 (a transition's fade and a pose'sFadetoo) fades an element with children as one group, as CSS's group opacity (ui/group.go): the subtree renders into the same transient canvas, the areakeptLayer.areagives within the clip, drawn as one image at the opacity (paintGroup), so every renderer draws it as it draws an effect's, and kept asCachekeeps its layer while what it holds is the same (keptLayer.draw). A layer costs a render pass, so a group paints flat first, its opacity multiplied into each op as before, andcheckFlatlooks at the ops it painted (overlapping: their bounds within the clips pairwise, at most 32 of them, so that what culling skips counts for nothing): when two overlap, it renders the layer, takes the flat ops back (takeBack: the scene's slices, its canvases and the parts drawn,engine.drawn) and paints the layer in their place. The state'sgroupedthen has the next frames go to the layer at once, back to flat when it no longer overlaps, so the frames before change what a group costs, never what shows. Holes (web views) and effects reading their backdrop keep a group flat, as no layer shows what is behind it. The code is linked byOpacity,TransitionandAnimatePose(linkGroups). A blurred or kept element fades its canvas at its opacity rather than each op in it, and paints nothing faded out.ui/shadergives an effect's entry point (ColorEffect,DistortionEffect,LayerEffect) aFragmentthat calls it, and the bridge reads the content as image 3 (LayerImage) from where the box's corner lies in it (__layerInset). A shader reading its backdrop (backdropAt,backdropSample) is an effect withBackdrop:Painter.EffectBackdropcomputes the backdrop as the builder will (scene.BackdropOfof the op's bounds), the bridge maps positions to its texels (__backdropRow0,__backdropRow1,__backdropArea), and the renderers bind their backdrop texture as its image 2 (gpu.BackdropImage); the CPU twin readsscene.BackdropImage. Direct3D's compiler takes from 0.1 s to more than 1 s for a shader's effect (the renderer's head and tail, with its coverage, are most of a small one's; optimization levels barely change it), soui/shaderstarts it as it compiles one (d3d11.PrecompileEffect), on goroutines, as many at once as the CPUs but one. The renderers share the code (compileEffectCode), waiting for what is left, and it stays in%LOCALAPPDATA%\MyGo\ShaderCache, a file named by the SHA-256 of what it compiled from, one directory for the user as Windows' own D3DSCache is, the least recently used removed beyond 32 MB. Effects compiled ahead of time, and apps without shaders, do not link it. On macOS,metal.PrecompileShaderdoes the same with the system's default device: Metal's library (about 85 ms the first time) and the pipeline for BGRA8 windows (about 60 ms), which renderers on that device retain (sharedShader) in place of making them; Metal's own disk cache keeps them for later launches, and wide-gamut windows make their pipeline from the shared library. -
Corners. On macOS, rounded corners are continuous, as AppKit's and SwiftUI's (
scene.Op.Continuous), and drawn as Core Animation draws them on screen, with the function of its shaders (QuartzCore'ssupercircle_sdf), which differs from the Béziers of SwiftUI's paths by up to half a percent of the radius: a quarter circle around the diagonal that bends less and less toward the edges, which it meets 1.528665 radii from the corner, a quartic of the ratio of the point's coordinates in the box of the curve. On sides too short for both corners' curves, the curves blend toward quarter circles by the side's clamp factor, from the side's length over its corners' mean radius, so a circle's corners are quarter circles; a shape is the intersection of its corners', so a curve may reach past the middle of a side whose other corner is smaller; and edges are antialiased as Core Animation does, by the value over the sum of its derivatives (internal/raster/corner.go). Against captures of layers on screen, every pixel is within 1/255 for corners that fit and for circles, and within 6/255 for pills and other short sides at the sizes of controls. The CPU's renderer finds where a curve crosses a row by regula falsi between the quarter circles of the radius and of the curve's extent, and keeps the rows' spans of a clip with continuous corners for the operations within it; a whole frame ofBenchmarkFrametakes a fifth longer. Elsewhere corners are circular, as Windows and GTK draw them, and only the CPU's renderer and Metal's draw continuous ones. -
Text.
internal/textlays out text with the system's own text stack, behind a smallengineinterface: DirectWrite on Windows (IDWriteTextLayout, with anIDWriteTextRendererimplemented in Go collecting its glyph runs), Core Text on macOS (CTTypesetter, withNSFontfor the system font's weights) and Pango on Linux (with fontconfig and HarfBuzz underneath, as in GTK); in browsers, a canvas's 2D context (canvas_js.go, see Browsers). They find the fonts, fall back to other fonts for what one lacks, shape, run the bidirectional algorithm and break lines; font files stay memory mapped and shared with every other process. A text wrapped again at another width, as in a window being resized, keeps itsCTTypesetterwhile frames use it, and gives it up 8 frames after the last, or as windows go idle. Core Text opens a break iterator for each line of text with a language attribute, solanguageShapesleaves the attribute out where it changes nothing. A number, new in each frame on a dashboard, is put together from the glyphs and kerning of its characters, measured once for each font and checked against Core Text or DirectWrite from time to time (numbers.go). Latin text kept across widths is shaped into one line that its lines take their glyphs from, breaking at the spaces before plain words, checked in the same way (wrap.go): Core Text is asked for every other break, and DirectWrite, which shapes a whole text before breaking it, lays out a text with any other break, keeping the one line 8 frames after the last, or as windows go idle, and drawing the spaces ending a line as a run of their own, as DirectWrite does. Korean text breaks between words, as CSS'sword-break: keep-all(keepWordsWholetakes the breaks between two letters or digits out of Unicode's): Core Text moves the break it suggests back to the last one left in the line; Pango is told not to break within the words (pango_attr_allow_breaks_new), and lays out again from a line a word longer than it overflows, without that word kept; DirectWrite's lines, when one ends within a word, are broken anew from its cluster metrics and shaped one by one; the canvas engine leaves those breaks out of its own. Every engine gives a line's glyphs in visual order, from the left, through right-to-left runs too (DirectWrite's runs, whose glyphs come in logical order, are reversed), so that what walks them, andui.Shape's callers, rely on one order. The package assembles their lines into layouts, with its own whitespace trimming, ellipsis, alignment, carets and selection, so those behave the same everywhere, and caches layouts by their parameters. A text that wraps none of its lines lays out alike at any width they fit in, but for where its alignment puts its lines:LayoutFromlays it out at another width as the layout it is given (uigives an element's last), or as the text's layout without a width, once made, sharing its lines, or copying those its alignment moves (realign), so that a text whose box grows or shrinks, as a transition or the window resizes it, is shaped once, and one measured, then laid out at the width it gets, too. The engines rasterize glyphs too (IDWriteGlyphRunAnalysis,CTFontDrawGlyphs, cairo) into a coverage atlas, color glyphs (emoji) into a color atlas, and subpixel glyphs, the coverage of each pixel's red, green and blue, into the color atlas as well. Text looks as each platform's own toolkit draws it, pixel for pixel, which tests against AppKit, Direct2D and GTK 3 labels showed:- Placement.
System.Glyphtakes a glyph's pen position and places it as the engine does: Core Graphics' subpixel quantization, which AppKit leaves on, atmin(5, ⌈100/(3·px)⌉)positions a pixel for glyphs ofpxpixels an em, left of the pen; DirectWrite's six positions a pixel for ClearType, eight in natural, four in natural symmetric and two in downsampled rendering, whole pixels in GDI and aliased rendering, the nearest to the pen; GTK's whole pixels, as Pango rounds glyph positions.System.Baselinesnaps baselines likewise: down to the next pixel on macOS (Core Graphics floors in its y-up space), to the nearest on Windows, up on Linux. - macOS. Core Text draws with font smoothing, as AppKit does unless
the user turned it off (
AppleFontSmoothing, read throughNSUserDefaultsas AppKit does: Core Graphics' bitmap contexts ignore it), which emboldens glyphs more the lighter their color is, in four steps of its luminance: its glyphs are rasterized for eachtext.Shadeof the text's color.text.Thick, for text aui.Fontthickens (the terminal'sFont.Thicken, as Ghostty's font-thicken), smooths at the strongest step whatever the color and the user's setting, andtext.Flat, for text aui.FontdrawsAntialiased(as browsers draw-webkit-font-smoothing: antialiased), does not smooth; other engines draw both as other text. - Windows. Glyphs take the rendering mode and grid fitting DirectWrite
recommends for their font and size (
IDWriteFontFace3's, which from about 1.9 pixels a DIP downsamples natural symmetric rendering, as Direct2D draws), ClearType where the system smooths fonts with it (SPI_GETFONTSMOOTHINGTYPE, with the pixel geometry and ClearType level of the system's rendering parameters) and the glyph is on an opaque background (the root's, or that of an element around it holding all of it that shows, as a pane beside a sidebar over a material), and are aliased where the system does not smooth fonts. Renderers blend them as Direct2D does, with the gamma and enhanced contrasts of the system's rendering parameters (scene.TextParams, Windows Terminal's reproduction of Direct2D's shaders), and half again as much contrast for the families Direct2D finds too thin for antialiasing, such as Courier New. - Linux. Pango takes GTK's font options, which package ui reads from
gtk-xft-*(platform.Theme.FontRendering, given toSystem.SetFontRenderingwith the interface font): the desktop's antialiasing, subpixel order and hint style, with metrics hinted to whole pixels and glyph positions rounded, as a GTK 3 label has them. Subpixel antialiasing draws subpixel glyphs. - Decorations.
System.Decorateplaces underlines and strikethroughs as the platform does: Core Text's own geometry for AppKit, snapped in points, an underline as low and thick as the fonts under it want, skipping the ink of descenders (but a link's, on hover:SolidUnderline), a strikethrough through the middle of the x-height, from the text's start to its width rounded up to a point; Direct2D's, an underline at the font's offset rounded to whole pixels from the snapped baseline, a strikethrough from the baseline as laid out, with edges where its eight samples across and down a pixel put them; GTK's, at Pango's metrics in whole pixels, the underline along a run's ink and advances, the strikethrough along its ink.LineStyleoverrides the font: a thickness (DecorationThickness) around the line's middle, and an underline's offset (UnderlineOffset, CSS'stext-underline-offset), its top that many DIPs from the baseline the engine draws on, in whole pixels, for every engine, the browsers' canvas included; Core Text skips the ink where the offset puts the underline. - Joined glyphs. Renderers blend each glyph in turn, as Core Graphics
and Direct2D do, but cairo adds the coverage of a run's glyphs before
blending it, which differs where antialiased edges of neighbors share
a pixel. There, on Linux, package ui has the text system join them
(
System.GlyphRun): their coverage added and clamped, cached as one bitmap. - Strokes.
TextStrokedraws each glyph's stroke as a mask of its own (System.StrokeGlyph), made in Go from the glyph's coverage for every engine: the coverage dilated by half the width less the coverage eroded by it, each edge pixel's coverage placing the edge within it, so the stroke is centered on the outline, round at its corners and as smooth as the glyph; color glyphs stroke their alpha. Cached with the glyphs, under a key with the width, and drawn by every renderer as any mask glyph, all of a text's before its glyphs (paint-order: stroke fill).TextStrokelinks it, so that apps that stroke no text do not. - Shadows.
TextShadowandSpan.Shadowdraw each glyph's shadow as a mask of its own (System.ShadowGlyph), the glyph's coverage (the alpha of a color glyph) blurred in Go by a separable Gaussian of sigma half the blur, as CSS has it, padded by three sigmas and capped at 64 pixels. Cached with the glyphs, under a key with the sigma, and drawn as a mask glyph at the offset, all of a text's before its strokes and glyphs.NewTextShadow, whichElement.TextShadowand aSpan'sShadowtake, links it: aTextShadowhas no fields to set otherwise, so a shadow that paints is one that linked it. - Tabs.
Style.TabSizesets tab stops every that many spaces of the style's font, letter spacing included, from the start of each line, which each engine takes natively: Core Text's paragraph style (no tab stops but aDefaultTabInterval), DirectWrite'sSetIncrementalTabStop, a Pango tab array of one stop, which it repeats, and the canvas engine's own lines, which otherwise measure a tab as the canvas does, as a space. The firstTabSizelinks it (System.UseTabSize), asJustifydoesUseJustify. Paths drawn withPainterare masks in the coverage atlas, rasterized byinternal/vec, cached by their shape relative to the pixel grid in quarters of a pixel, to which their points move first: a path looks the same whichever path of its key drew its mask first. A path too large for one mask, over 4095 pixels a side, draws as tiles of it (pathTiles), each a mask of the whole path clipped to the tile: only the tiles that show and that its edges reach, and, inside a fill, tiles covered whole, which share one mask: those its edges wind around by its rule (Path.FillRule), which also picks the mask's (vec.Rasterizer.MaskEvenOddforEvenOdd) and keys it, as it keys a path's clip. A path larger than a tile (511 pixels a side) draws as tiles too when the atlas has lately had no room for a path's mask (pathCrowdedPaints): such a mask takes a shelf of the atlas of its own, mostly empty for a long thin stroke, and a few of them, as wires redrawn every frame, leave no room for the frame's other paths, which would not show. A path whose mask finds no room draws as tiles at once, and the runtime's repaint (System.MakeRoom) draws the frame's large paths as tiles, which pack eight to a row of the largest atlas: the room made lets go of the masks of those the paint drew before (System.Unkeep), which it would keep as the frame drew them, and which would leave the tiles no room. On the pixel grid tiles show the pixels of one mask; under a transform off it, a faint line where they meet.Path.Containscounts the winding of the path flattened at four times its scale; an element'sHitTest, kept in its state, is one more test ofengine.contains, after its box and its own clip. A stroke's mask is the outline of its center line, each edge drawn once (strokeInto): one side, turning at corners and ends as the path's joins and caps say (Path.Join,Path.Cap), and back along the other. Its dashes (Path.Dash) are cut from the center line as the mask is drawn, from where each polyline starts in their pattern, so that a trimmed path's dashes stay where they are, and its key holds that place, in quarters of a pixel within one pattern, with the caps, joins and dashes. As in Skia, a dash through the start of a closed subpath is one dash, and the point between a dash and a gap is in the gap. Square caps reach past half the stroke's width, and miter joins as far as the points of its corners do within the miter limit (flatPath.reach, from the points the mask draws), and the mask and its tiles reach as far. OpaquePainter.StrokePolylineon a GPU is the exception: each segment is a rotated fill with round ends, drawn as other fills are, without a mask; translucent ones, and the CPU renderer, take the mask's way, so that overlapping segments do not show darker. A translucent line of one segment overlaps nothing, and is a rotated fill too: the stripes of a diff's fillers are hundreds of these, whose one mask would be as wide and as high as the filler in view is high, rasterized again as that changes, and drawn that large for each stripe. So are icons:internal/svgparses SVG documents (withencoding/xmland its own small CSS cascade) into nodes of paths, paints and layers, and draws them on the CPU withinternal/vec, compositing gradients, group opacity, clip paths and masks in layers of premultiplied pixels;ui.Iconkeeps the coverage of that drawing for each size, which the GPU tints like a glyph, andui.Imagethe colored pixels, as images whose textures it updates in place when only the text color changes. An atlas logs the rectangles that change, so renderers upload only those. DirectWrite methods taking floats are called through purego, which sets the floating point registers thatsyscallleaves alone on ARM64. Letter spacing and OpenType features go to each engine's own, so lines break with them:IDWriteTextLayout1's character spacing and anIDWriteTypography, Core Text's tracking and a font copied with feature settings, Pango's attributes. So do the spans of rich text, which style ranges of runes (text.Span): DirectWrite's setters over a range, the fonts of a Core Text attributed string's ranges, Pango's attributes between byte indices.Params.Spansholds them encoded as a string, soParamsstays a key of the layout cache. A span's letter spacing of 0 is the text's unlessSpacingSet(ui'sSpan.NoLetterSpacing, or an inline element'sLetterSpacing(0)) makes it its own, which each engine sets over the span's range as any other. Laying out reuses what shaping throws away: engines take the glyphs, runs and linesshapereturns from a scratch each layout resets (shapeScratch), as the layout copies them into its lines, which share one allocation; Core Text keeps its buffers, the attributes of each font, and calls the functions of each layout throughSyscallNrather than reflection. Colors, underlines and strikethroughs of spans only paint, by the rune each glyph starts. A span with anObjectWidthmakes its object replacement characters inline objects: boxes standing on the baseline, which lines break around and which draw no glyph (Glyph.Object), as a rich text's images, or a text's math withElement.DrawObjects, which paints them after the glyphs. The line takes their height, and their descent (ObjectDescent, encoded only for objects), in the shared layout; each engine only gives them their width: Core Text through run delegates, whose callbacks return floats Go cannot, so they are leaves in assembly (rundelegate_darwin_*.s); DirectWrite throughIDWriteInlineObjects, their places found by hit testing, as its glyph runs' are; Pango through shape attributes; the canvas engine as clusters of their width. A span'sBaselineShiftmoves its glyphs and objects off the baseline in the shared layout too, as the room the line takes for them and the baseline its decorations are drawn from (decoRange.shift). The engines break a paragraph at one width, which is all DirectWrite's layouts take, so what else paragraphs need is the shared layout's (paragraph.go), alike on every engine. A first line's indent (Params.Indent) breaks that line narrower, then the rest anew from where it ends. Justification (Params.Justify) widens the spaces and the room after Han characters and kana of the lines the width wrapped, but the last of each paragraph, moving each glyph by the room the clusters left of it gain, so that carets and selection follow; it is linked in once aTextShapeasks for it (System.UseJustify), as apps that do not justify text need none of it. The engines all break lines at soft hyphens (U+00AD) and draw them as they please: the layout hides those within lines (a glyph of no font and no advance, which keeps the caret's place) and draws a hyphen, the engine's glyph for U+002D in the soft hyphen's style, at the end of a line broken at one. Only Pango leaves room for it as it breaks, so a line whose hyphen does not fit is broken again, narrower by the hyphen, where Unicode's line breaking allows (else the hyphen overflows, as a long word does), and the rest anew (breakLines). The padding of a span (Span.PaddingX), which engines know nothing of, is the shared layout's too:padLinemoves the glyphs of each line's piece of the span in, and those after it on, by the padding at both ends of each piece, as CSS'sbox-decoration-break: clone, and widens the line; a line that does not fit with the padding of its pieces is broken again, narrower by it, and the rest anew, and an ellipsis leaves room for it. A paragraph without an indent, a soft hyphen or a padded span is shaped once.Params.Balance(Element.Balance) breaks a paragraph that wraps at narrower widths, bisecting to a quarter of a DIP, for the narrowest that keeps its count of lines (System.balance);Params.Pretty(Element.Pretty) breaks again only its last two lines, when the last is a word alone, bisecting for the widest width at which the line before gives it a word and they stay two, at a space (System.prettyLines), whichElement.Prettyand aTextShapethat asks for it link (System.UsePretty). The layout cache keys both with the other params, so a build lays it out once. Text elements built inside a text (ui/inline.go) are inline: the paragraph takes their text when itsChildrenreturns, and their styles as spans over its own when it lays out. At commit, each gets the boxes of its runes in the paragraph's layout (Selection), one hit each, so the pointer, the focus order, tooltips and menus work as for any element; they paint only their focus ring (theirOpacity,Invisibleand dimming fade the colors of their spans,Span.faded, and a color faded to nothing stays the span's own,colorSet, so the paragraph skips their glyphs but keeps their advance), and assistive technology sees those that are more than text, such as links, as nodes inside the paragraph's. Fontconfig knows no desktop's interface font, so on Linuxsystem-uistands for the family of GTK'sgtk-font-namefirst (platform.Theme.UIFont, which package ui givesSetUIFamilywith each change of the appearance).
- Placement.
-
Atlas lifetime. A mask drawn for the first time goes to the transient zone at the bottom of the atlas, which the next frame frees, and moves to the lasting zone at the top once another frame draws it, so animated shapes never fill the atlas. When an atlas fills up during a frame anyway, the engine calls
MakeRoom, which repacks what the frame drew, grows the atlas when that is much of it (what the frame left out counting once, however many times it draws it) and forgets the rest, and paints the frame again: no frame shows with glyphs missing. The mask atlas starts at 256×256, and the color atlas at one transparent texel until a color or subpixel glyph draws. Growth accounts for the width and height of missing bitmaps as well as their area, so long thin paths fit on the repaint too. The first glyph can grow an empty atlas immediately, without invalidating anything already painted. -
Renderers.
internal/gputurns a scene into instanced quads, in batches that share a scissor rectangle and an image, for a shader that computes the signed distance to rounded rectangles, Evan Wallace's blurred rounded box, gradients, stripes, the dashes of borders, atlas coverage and the innermost rounded clip; outer clips are scissor rectangles, and a clip reaching into an outer one's rounded corners cuts with that one's mask as well. The builder omits operations wholly hidden by the certainly opaque core of a later fill, keeping at most eight occluders and treating backdrop-reading effects as barriers. Its planner shares clipping rules with drawing, grows shadow support before transforming it, and skips scenes without potential opaque fills. Large shadows with a caster and border-only fills draw as scissored strips that leave out where their output is zero: inside the caster, between a border's edges, and outside a border's outer edge. With round corners they draw in bands of rows (gpu/bands.go), each drawing only the columns between where the outer edge may reach and where the inner one certainly covers, so a circle a pixel wide shades about its length rather than its box; the bands across the corners are as many as pay for their draw calls (bandCost). With continuous corners they draw around one rectangle. These scissors never change a clip mask's sampling coordinates. Both transformations are shared by all GPU backends. Large fills, shadows and images with continuous corners (negative radii, which only Metal gets) draw as nine-patches (gpu/corners.go): their corners' boxes as they are, and the cross between them as a twin with square corners, in batches whose pipeline lacks the curves' code. Away from the boxes the shader computes the same value either way: a fill's pixel area, and a blurred shadow whose samples miss the curved rows or whose blurred sides round to 0 and 1 (9 sigma from the curve). Each piece is a draw call of its own (its scissor rectangle), so a shape is cut only where the twin's pixels, within the shape's quad, save more than the pieces' calls cost (pieceCost): small shapes, near their corners everywhere, stay whole and join their neighbors' batches. Metal opts into three further builder capabilities: marking fills whose clip coverage is one, splitting opaque cores from blended edges, and batching by fragment class, the last of which Direct3D 11 takes too. OpenGL keeps the original instance flags and class-independent batching. Metal binds the instance buffer once a pass and draws each batch with its first instance as the base instance, which[[instance_id]]includes; a batch binds its pipeline, image, clip mask and scissor rectangle only when they changed, with selectors looked up once, and the scissor goes by pointer (setScissorRects:count:) rather than through a reflective purego function: messages from Go cost about half a microsecond each. An effect draws in a batch of its own, with a pipeline of its own, which the renderer makes the first time the effect draws; one reading its backdrop draws after a backdrop step (Builder.Backdrops): the renderer ends its pass, reads the backdrop's area of what it drew (Metal from the drawable, which is notframebufferOnly; Direct3D 11 and OpenGL from a copy of the area,CopySubresourceRegionandglCopyTexSubImage2D, whose rows go up), averages and blurs it with two small pipelines (downandblur, scissored to the texels they compute, reading them withLoad,readortexelFetch) into textures as large as the frame's largest backdrop, and goes on with the pass, the backdrop bound for the effect. An image drawn blurred goes through the same two pipelines before the frame's pass, which the builder asks for as it meets it (Builder.BlurredImage), from the image's texture into one of the frame's own, in BGRA8 whatever the frame's format, reading what lies beyond the image as transparent (gpu.PassClear); OpenGL reads canvases flipped and uploaded images not (gpu.PassUnflipped). The stops of gradients travel in a texture of 32-bit floats the builder fills each frame (Builder.Stops, two texels a stop: its color, then its place and its color in Oklab), bound to texture 9 (t10 in Direct3D), from which the shaders'stopsAtmixes them asinternal/rasterdoes. Each renderer draws scenes offscreen for the tests of packages defining effects (RenderOffscreen,NewOffscreen). -
Canvases (
scene.NewCanvas,ui.Canvas) are images drawn from a scene of their own.Canvas.Drawpaints into its scene with a painter of scale 1 sharing the window's atlases, and lists it in the frame'sScene.Canvases, inner canvases first as they finish. Before a frame, each renderer draws the canvases it lists or shows whose version its texture does not hold (Scene.EachCanvas, those a canvas shows before it), each with a builder of its own, into a texture in the frame's format before effects: Metal into a BGRA8 render target in the frame's command buffer, Direct3D 11 into a BGRA render target view (the dynamic instance buffer, mapped with discard, serves all), OpenGL into a framebuffer object, whose rows go up, so that image instances showing it flip their texture coordinates (the samplers take source rectangles either way round). A canvas's texture lives as long as the canvas (weak.Pointer), not drawn again when shown; a transient canvas's, drawn anew in every frame that shows it, as an effect's, only while frames draw it: the renderers' sweep every 120 frames frees it, rather than wait for the canvas to be collected, which the little of Go's heap it takes may put off for minutes while its texture takes megabytes. The CPU renderer draws canvases into theirPix(PixVersion). Near square corners, coverage is exact, the area of a pixel inside the box, so lines thinner than a pixel cover as much as they should, as text decorations need. A fill's border widths travel in its texture rectangle, which fills do not use, and a glyph's gamma ratios and contrast in its radii. Each instance is thirteen float4s, including its two affine rows. The shader writes a second color, the source's alpha of each channel, and renderers blend with it (dual-source blending): it is the color's alpha but for subpixel glyphs, whose subpixels cover each channel by its own. OpenGL ES withoutEXT_blend_func_extendedblends the mean of their subpixels instead. A hole is an opaque fill in a batch of its own (Batch.Hole), blended with a source factor of zero: the destination keeps one minus its coverage, as the CPU renderer'serasedoes. Every GPU renderer draws these instances:internal/gpu/d3d11with a shader compiled to DXBC ahead of time (on Windows, with the system'sd3dcompiler_47.dll), so apps carry no shader compiler. The generated file records the SHA-256 of the source it came from, line endings aside (gpu.SourceSum), as Metal's does: bytecode older thanshader.hlslfalls back to compiling that with the same DLL, which Windows has, and its test fails. Both compilation paths enable IEEE strictness with optimization, avoiding a native compiler optimizer crash on transformed coverage sampling. Batches of a class (gpu.Class) draw with shaders of their own (classes.hlsl, aftershader.hlsl, whose functions they share, which leaves the sources effects are compiled from as they are): instances of a class are not transformed, so they leave out the general shader's coordinates, pass on 6 to 10 values rather than 15, skip the clip where the vertex shader finds it holds the whole quad, and write one color, blended by its alpha, but for subpixel glyphs, which need dual-source blending. A plain fill takes 83 instruction slots rather than the general shader's 3,627. A set of classes draws with the first ofclassShadersholding them, made as first drawn: each class, the sets that small batches join most, and all of them, which still costs less than the general shader, which only transformed instances and effects need. The renderers of a process share one Direct3D 11 device and its immediate context (shared.go), with the shaders, input layout and states, which never change: a device costs the driver tens of megabytes, which a window each would multiply (#1517). Each keeps its swap chain and what its frames change, and the device goes with the last of them. They draw on the main thread, each frame binding all it draws with; the device is multithread protected (ID3D10Multithread) all the same, so that DXGI's calls on a swap chain from another thread are serialized with the context's. A renderer made after the device was removed makes a new one; those holding the old one fail their next frame (Render), and their windows make them again on the new one, as after any failed frame. A device made with WARP gives way the same way to one on the GPU once a renderer made can make one, as a window drawing in software tries from time to time. It draws into a flip-model swap chain on the surface's window, or, over a material, one for DirectComposition shown on it, with WARP when no hardware device works. At most one frame waits ahead of the screen, not DXGI's three, andPresentdoes not block for it: the swap chain's frame latency object (latency.go) tells room for a frame, and a frame drawn before there is presents once there is, as a goroutine waiting on the object has the main thread present it (SetPost); frames paced by the display (WM_PAINT, but a resize's andFrameNow's) are not built meanwhile (Renderer.Ready). Every present takes one of the object's signals, so its count stays the frames the screen can take. Frames that make the refresh without building ahead, as the last ones' cost tells (frameCost: the main thread's time and the GPU's timestamps, taken after the frame's clear, which waits for the window manager to give the buffer back; a frame noReadystarted, as a resize's, counts none, its main thread's time not known), build once it composed the last (DwmFlush) and there is room, and present with sync interval 0, to show at its next composition. Heavier frames build ahead and present with sync interval 1, which keeps them at the display's pace a refresh later. A frame of a new size is presented at once (sync interval 0), not after the frame queued before it, and waits until DWM shows it (DwmFlush), so a live resize shows no stretched frames: the window takes its next size with the frame of that size. DWM may still show the window at its new size a refresh before that frame (#222), so the window's swap chain shows its frames unscaled (DXGI_SCALING_NONE) with the content's background beyond them (SetBackgroundColor, fromscene.Scene.Background), and the window paints what it grew by around its surface with that background before fitting the surface to it (platform.BackgroundSurface,fillUncovered): what shows meanwhile is the background, not black. As the window shrinks, the surface takes the new size and presents its frame before DWM takes the window's, so DWM may show the window at its last size for a refresh, and around the surface the window's own pixels where the surface was, which the swap chain covered: the window paints them with the background too, under the surface, at each size (fillUnder), unless its frames are drawn in memory onto them. Until a frame of its content shows in it, the window is cloaked (DWMWA_CLOAK, from the surface's creation): shown, it would show white while the renderer starts and the first frame draws, a few hundred milliseconds (#905). Windows sends itWM_PAINTall the same, and uncloaks it after the one whose frame reached the surface (platform.FirstFrameSurface), or a second after it was shown without one (a timer, which only an idle main thread lets fire); a refresh of DWM between the present and its frame showing shows the background under the surface (fillUnder). A window whose frames show through DirectComposition (no redirection bitmap, as a flyout's, or web views over the frames) has no pixels of its own there, and DWM would show it black for a refresh or more: it stays cloaked until DWM composed the frame (DwmFlushon a goroutine, within four refreshes at 60 Hz), then uncloaks on the main thread (#1591). A window made hidden gets noWM_PAINT: its first frame is drawn while it is hidden (drawHidden), which makes it ready to show, and it uncloaks after its firstWM_PAINTonce shown; a present that DXGI reports occluded leaves the next frame to draw whole. Resizing the buffers takes DWM longer than a frame for a window about as large as a 4K screen, so while the window changes size the swap chain has three buffers a quarter larger than the window, until a frame a second after the last change, which a timer asks for, gives it two of the window's size again. The unscaled swap chain shows those buffers whole, from the top left corner, and the window clips them: showing only the window's size of them (SetSourceSize) would stretch that part over the buffers' size, magnifying the frame (#892), as it does for a swap chain for composition. A frame drawn whole fills what is beyond it in the buffers with the content's background (clearWhole), which shows where the window grows within them. A swap chain that stretches, where the unscaled one is refused, shows the window's size of the buffers. The swap chain flips in sequence (FLIP_SEQUENTIAL), so that a frame draws only what changed since the last, as Metal's (below): DXGI keeps what each buffer holds, and as a frame is presented with the rectangles it drew as its dirty rectangles (Present1), copies into it what the last frame changed outside them, so there is no buffer age to track. The frame clears the rectangles (ID3D11DeviceContext1::ClearView, which Direct3D 11.1 has, WARP too) and draws the batches scissored to each; a frame that changed nothing is not presented. With WARP, on a runner without a GPU, a caret blinking in a window of 1280×760 pixels took a tenth of the CPU's time it took drawn whole;internal/gpu/metalwith a shader in Metal Shading Language compiled into a Metal library ahead of time (on macOS, with Xcode'smetaltools), which spares a first launch the 100 to 150 ms Metal takes to compile the source until it has cached it; a library older thanshader.metalfalls back to that, and its test fails. It draws into a CAMetalLayer it adds to the surface view's layer. The command queue, shaders, sampler and placeholder texture are made on demand. A frame that changes the layer (its size, scale or pixel format), or shows web views, which move in the window's transaction, presents with the Core Animation transaction (presentsWithTransaction), so a live resize shows no stretched frames; the others present as soon as the GPU has them (presentDrawable:), sparing a pass of AppKit's display cycle and a commit of the transaction each: an animation at 120 Hz takes about half the main thread's time it would. A resize's frame that AppKit asks for (updateLayer) after one presented with the transaction, outside a live resize, draws at the display link's next tick, as for a window resized from its view, or right after the frame that took back the drawables given back when idle: drawn in AppKit's display of the view, presented with the transaction too, it did not show, and the window kept the last frame stretched to its new size (#1044). So does one asked for after a pause, which would otherwise draw from the backend's queue (drawSoon) in a pass of its own, and not show either. The main thread never waits for the display: after each frame presented as soon as drawn, a goroutine on a thread of its own (present.go) takes the next frame's drawable, whichnextDrawableholds back while the display holds the others, most of a refresh at times. A frame that comes while it does is built at once, its canvases and instances ready, and waits (pendingFrame); once the goroutine has the drawable, it has the main thread (SetPost, throughsurface.Conn.Post) draw the frame into it and present it. Frames of the display link's ticks (platform.SurfaceEvent.Paced) are not built while one waits (Renderer.Ready,windowHost.waitDisplay): the next draws once it was presented (platform.PacedSurface.FrameNow), at the time its tick asked for it, so that what moves keeps its pace; the link keeps ticking meanwhile. Other frames, as a resize's, draw the waiting one first. Frames presented with the transaction, which change the layer, take their drawable on the main thread, as the first after a pause does, after any being taken. Building a frame before its drawable comes, as blocking for it did, keeps the GPU as busy: building only once the drawable came drew a sixth fewer frames where the GPU took most of each refresh. Instances use two frame slots, for the main target and each canvas, and a slot waits only for the command buffer that last used it. Updating an atlas or image texture in place still waits for every frame in flight. Rendering a canvas into its texture stays ordered on the command queue. The layer keeps three drawables, as Core Animation makes while a window animates: with two, on a ProMotion display, frames waited for one most of each refresh, and a fifth of the frames went undrawn. The third costs a frame of memory while a window animates. Waiting for a drawable remains separate from waiting for an instance buffer, so offscreen encode timings do not establish presentation latency. Opaque fill cores use an unblended pipeline in BGRA8; float16 keeps blending to preserve its interpolation rounding, and GPUs outside the Mac2 family keep to the blended one: one such GPU, an Intel Haswell whose driver OpenCore Legacy Patcher backported, hung in the unblended pipeline (#1566), and blending them costs only that GPU's tile-based win of writing without blending. Fragment functions are specialized by batch class and continuous corners, with general shaders as an identical fallback. Runs of small batches of different classes (each covering at most 128×128 pixels, as a box's shadow then its fill) join into one batch whose function holds just their classes' paths (Batch.Joined): a draw call costs the CPU more than those pixels cost the joined function. A worker compiles and warms these pipelines, cached per device, outside the frame path. Renderer release stops and joins that worker before releasing its shader library. Float16 shadows stay general, and fills with fractional opacity avoid the plain class, because specialization otherwise changes rounding. GPUs outside Metal's Mac2 family (supportsFamily:), as Intel's Haswell, draw every batch with the general pipeline, compiling and warming none, and keep their batches together: one such GPU, on a driver backported by OpenCore Legacy Patcher, hung while specialized pipelines compiled (#1566). Function constant indices 0 through 7 belong to the renderer; effects start their own at 8. Two seconds after the last frame, as the driver frees its own memory of frames, a timer shrinks the drawables, which frees all but the one shown, until the next frame makes them again, and releases GPU instance buffers, atlas/image textures and backdrop textures: an idle window keeps one frame of memory. Its drawables are notframebufferOnly, as effects sample the drawable for their backdrops. A frame draws only what changed since its drawable was drawn. Core Animation hands the layer's drawables out in turn, and each holds the frame drawn into it last: the renderer keeps, by the drawable's texture (retained, so that no other takes its address), what changed in the frames since (gpu.Ages, as EGL's buffer age says), found by comparing each scene with the last (scene.Differ, as the CPU renderer does), grows it by the backdrops of the effects it meets, and, while that is less than half the frame, loads the drawable rather than clearing it, copies a texel cleared to the frame's color over the area (with thedownpass, so that the GPU converts the color as it does clearing), and draws the batches scissored to it, leaving out those outside. The area is up to eight rectangles apart from each other (gpu.Part, from the Differ's, whichgpu.Ageskeeps for each drawable, those that meet or nearly do merged, and past eight the two whose union grows least): a spinner in one corner and a progress bar in the other redraw themselves, not all between, each batch drawing once in each rectangle it meets, so that none is blended twice. A backdrop an effect reads is within one rectangle, drawn before the effect reads it. Apple's tile-based GPUs then load and store only the tiles the pass draws in: a caret blinking in a window of 3200×2000 pixels takes 0.4 ms of the GPU a frame rather than 2.4, and a spinner 0.4 rather than 2.9, on an M5. Copying the last frame forward instead (a blit) costs about what drawing it whole does, 0.34 ms against 0.43 at 3456×2000. A frame whose scene is the last one's draws nothing and takes no drawable. Apple does not document that drawables keep what was drawn: a drawable not seen yet, the drawables of a new size or format, and those after the timer shrank them draw whole, andMYGO_PARTIAL=0draws every frame whole. The pixels are those of the frame drawn whole, byte for byte: an op that the builder puts under another pipeline than in the last frame (a batch specialized for other classes, a pipeline made meanwhile) draws a unit in the last place apart here and there, so its bounds count as changed (pipelineDamage), as do those of a clip that starts or stops holding an op drawn under a transform, which it then cuts with a mask (scene.Differ);TestPartialFramesMatchWholedraws random scenes changing frame after frame into three targets in turn and compares each frame with it drawn whole. Colors outside the sRGB gamut (ui.Oklch, as CSS'soklch()) are in a table of the scene (scene.Scene.Wide), which ops and glyphs point into with a 16-bit index (Op.Wide, in room the op's other fields leave); theirColor,Color2andBorderColorhold the nearest sRGB colors, found as CSS Color 4 does (internal/gamut), which the CPU, OpenGL and Direct3D renderers draw, asgpu.Builderdoes unlessWide. The window host decides when a window draws them (wideHold): on a screen showing more than sRGB (platform.WideGamutSurface,canRepresentDisplayGamut:on macOS), from a frame with some until none came for two seconds, so that a blinking caret does not switch formats at every blink; those frames are the GPU's, and a window on an sRGB screen keeps drawing the nearest colors on the CPU. The Metal renderer (SetWide) then switches the layer toRGBA16Floatin the extended sRGB color space, which keeps components outside 0 to 1 and which the system shows in the display's gamut, and draws with pipelines and backdrop textures of that format, made the first time. A uniform of the frame (the z of the vertex and fragment globals) tells the shaders: Oklab gradients then keep what their mix has outside sRGB, and effects read it ase.wide, as the glass does to take its tint in extended sRGB (ui.Painter.EffectColor);internal/gpu/glwith the shader in GLSL 3.30 or GLSL ES 3.00, which the driver compiles when the renderer starts, since these versions have no compiled form every driver takes, and drivers keep what they compiled between launches, as Mesa and NVIDIA's do. It draws into the framebuffer of the GtkGLArea, which GTK shows. GL functions come from libepoxy, as GTK's do. Instances draw from per-instance attributes, pointed at each batch's first since OpenGL ES 3.0 has no base instance. The area'screate-contextasks GDK for OpenGL 3.3, else OpenGL ES 3.0, as GPUs with OpenGL ES alone have, and so does the probe below (glContext). In browsers the same renderer draws with WebGL 2, which is OpenGL ES 3.0 (see Browsers): only where its functions come from differs,load_linux.goorload_js.go. A frame draws only what changed since the last (Renderer.Partial, withscene.Differandgpu.Pipelines, as Metal's), into a framebuffer that keeps what was drawn, so there is no buffer age to track: it clears the rectangles of the area with the scissor test on, whichglClearkeeps to, and draws the batches scissored to each. GTK keeps the GtkGLArea's framebuffer from render to render, but makes its buffers anew, holding nothing defined, whenever it allocates the area, at the same size too: aSurfaceResizehas the next frame draw whole (Invalidate). Once the area has a context, GTK composites the whole window with OpenGL and tells EGL what each paint changed (eglSwapBuffersWithDamageEXT) in the window's coordinates unscaled, where EGL takes the buffer's pixels from the bottom left: at a scale of 2, Mesa told a compositor that a change at the top of the window was in its lower half, at half its size. A compositor that redraws only what it is told changed (Mesa's llvmpipe tells it whole frames changed, a GPU's buffers what GTK said) then showed title buttons the pointer hovered flickering or gone, and a shadow missing on some sides (#1071). At a scale of 2 or more on Wayland, the toplevel'sdrawsignal adds what GTK repaints (cairo's clip) aswl_surface.damage, in the surface's coordinates, to the commit of the swap that follows (damageGLPaint);TestGLDamageWaylandchecks it underweston --scale=2. A canvas's WebGL 2 context keeps its drawing buffer (preserveDrawingBuffer), which browsers then copy as they composite each frame drawn; in Chrome on an M5 a caret blinking still took a quarter of the GPU's time it took drawn whole, and a frame that changed nothing leaves the canvas alone. On Mesa's llvmpipe a caret blink in a window of 1280×760 pixels took a sixth of the time.
Linux draws with OpenGL only where it runs on a GPU. A GL context loads Mesa for good: some 50 MB of libraries (LLVM, which distributions' Mesa links, takes 19 MB as it loads), its threads and heap, as much as the rest of a small app; the first window of native UI loads it. Before a surface has one, the backend makes one context, on a window that never shows, and reads its renderer. A software renderer such as Mesa's llvmpipe (in virtual machines, in WSL without
GALLIUM_DRIVER=d3d12) redraws every pixel of every frame on the CPU, ten times the CPU renderer's work on an animated page; and once a window has a GL context GTK composites it with OpenGL, so the choice is made before any surface has one;MYGO_GPU=1skips it and draws with OpenGL wherever GDK makes a context, from the first frame, as the GUI tests of CI do on llvmpipe. The devices answer first where they can: no GtkGLArea without NVIDIA's devices or a render node of a DRM driver with 3D (simpledrm, bochs, VirtualBox's or Hyper-V's only show what the CPU drew), nor on WSL's device withoutGALLIUM_DRIVER=d3d12. A GtkGLArea shows only what OpenGL draws into it: when its GL renderer fails all the same, the frames drawn in memory go through OpenGL too (gl.Presenteruploads them and blits them into the area's framebuffer).internal/rasterdraws the same scene with the same formulas on the CPU, solid spans inside shapes and only the edges of shadows computed, and blends ordinary opaque mask glyphs in integers, retaining the general path for gradients, corrected/subpixel text and rounded clip edges. It redraws only what differs from the last scene (raster.Renderer, withscene.Differ, which compares the operations of two scenes from both ends), with the effects reading their backdrops that meets and those backdrops, in rectangles apart from each other (AddBackdrops): it is the renderer of tests, ofMYGO_GPU=0and of Linux where OpenGL would not run on a GPU, and the one a window falls back to when its GPU renderer fails. Frames that are not the surface's (a capture before the first frame) are kept, not drawn: OpenGL's context is current only in the surface's. An area drawn goes through the operations whose bounds (those the damage is found from) it meets; a large one is drawn on up to eight cores, in bands of 64 rows that each takes in turn, as rows differ in how much they draw, each pixel as drawing the area whole gives it. An effect is readied (EffectPixels.Begin) before its pixels, and one reading its backdrop reads pixels other bands draw, so an area is drawn up to each effect, its backdrop computed (backdrop.read, its rows and columns on several cores too), then from the effect to the next, in bands of 16 rows, as effects are costly and often short. The glass plugin'sBenchmarkGlass, a window of 1360×720 pixels with four panes, takes 6.4 ms drawn whole on an M5, 22 ms on one core. A whole frame ofBenchmarkFrame's view at 1844×2044 pixels, built and drawn, takes 0.7 ms rather than 3.2 on a Ryzen 7 8745HS: memory bandwidth and the lower clock of all cores keep it from scaling further.On Linux,
RequestFrameof a surface drawing in memory adds a tick callback to the area, which GTK's frame clock runs in its update phase, before painting: the frame is drawn there, andPresentDamagekeeps its pixels and asks GTK to repaint what changed (gtk_widget_queue_draw_area), which thedrawsignal paints from them in the same frame. GTK keeps the rest of its buffer, copying it from the last one into a new buffer when the compositor still holds the last, and tells the compositor only that changed: a digit of the counter copies 24 KB of pixels instead of 15 MB for half of a 4K display, in 0.01 ms instead of 1. A draw GTK asks for by itself, as a resize does, draws a frame in thedrawsignal, and repaints in the next frame what it changed outside GTK's clip. GTK neither paints a window that does not show nor runs its frame clock: until its first frame reached the surface (platform.FirstFrameSurface),RequestFrameof a window made hidden also adds an idle callback, which realizes the window, giving the area the size it will show at, and draws that frame in memory, showing it nowhere (drawHidden), which makes the window ready to show. A GtkGLArea has no context until it shows: the frame is followed bySurfaceRenew, so that the first in itsrendersignal makes the GPU renderer, which the hidden one could not.GTK's frame clock waits, after each frame, the refresh interval of that frame's timings, and 1/60 s while none has one. GDK fills it in only from the compositor: on Wayland in the frame callback of a paint it commits itself, which it never asks for when it composites the window with OpenGL (
eglSwapBufferscommits, with a swap interval of 0), and on X11 from a compositor sending_NET_WM_FRAME_TIMINGS. A GtkGLArea drew 60 frames a second on a 165 Hz display under Niri (#1219). The surface writes the interval of the monitor showing the window (gdk_monitor_get_refresh_rate, asked again each frame, so it follows the window to another monitor) into the timings of each frame it draws (paceFrameClock), unless they have one.GdkFrameTimingsis private, and GTK 3.24.24 moved itsrefresh_interval: the offset is the onegdk_frame_timings_get_refresh_intervalreads the write back from. A monitor of unknown rate, as Xvfb's, keeps 60 Hz, and a compositor's interval arriving later replaces the surface's. The clock still runs only while frames are asked for. On Wayland these frames follow a timer at the display's rate, not its refreshes.On Windows,
RequestFrameinvalidates the surface's window, whoseWM_PAINTcomes as soon as the message queue is empty, and frames drawn in memory show at once, through GDI or DirectComposition: nothing held them to the display, and a drawing asking for every frame (Painter.AnimationFrame) drew 5,000 a second on a 60 Hz display. After each, a goroutine waits for DWM's next composition (DwmFlush, throughplatform.RefreshSurface), for half a refresh at least, as it may return at once, and two at most, and paced frames (WM_PAINT, but a resize's andFrameNow's) wait for it as they do for the swap chain's room (refreshWaiter,windowHost.waitDisplay): the next draws once it returns, throughFrameNow, and the main thread handles input meanwhile.With a GPU renderer, every frame draws on the GPU, small changes too, so a gesture never switches renderers mid-way: on the CPU, a frame redrawing glass or a blur that content scrolls under takes 30 to 70 ms on a large window, where Metal takes 2 to 3. Every GPU renderer draws only what changed (above).
Two seconds after the last frame (
frameIdle), the host frees the frame drawn in memory, as large as the window, which the next frame draws whole, and tells the surface it is idle (platform.IdleSurface): Linux's calls glibc'smalloc_trim. GTK paints a window it composites with OpenGL into an image as large as what it repaints, the whole window for a whole frame, and glibc, raising its threshold for giving large blocks back as they are freed, kept one or two of them, 15 to 30 MB for half of a 4K display. These timers run on the main thread throughsurface.Conn.Post.A GPU renderer that fails (a driver reset, a GPU unplugged, sleep) is released and another made in the same frame (
ui/window.go): the GPU may be back already, and on Windows WARP stands in, presenting through a swap chain, as GDI cannot show frames once a flip-model swap chain has drawn into a window. Without one, frames are drawn in memory, and a frame tries again after a wait that doubles from one second to half a minute; a renderer drawing in software in place of a GPU the window had tries for it the same way. A window without a GPU at its first frame draws in memory for good. -
Tests.
ui.Testerruns views against a host in memory (ui/headless.go) with the CPU renderer, at its own time (engine.clock), which stands still untilTester.Advancemoves it. Its host runs no timer; it notes their deadlines, andAdvanceruns the frames due.Tester.AccessandAccessByRole(ui/tester_access.go) build the last frame's accessibility tree on demand, asaccessTreedoes for a window, and give its nodes asAccessInfo: the role an element was given (node.role), so that roles sharing a node's role stay apart, or the one following the node's in the order ofRole; a role added to both needs nothing there, a state a field. The fake backend's surface lets the core's tests drive content windows throughpackage mygo.BenchmarkFrameinuimeasures a frame of a large window on the CPU, andBenchmarkListScrollthe frames scrolling a list of a million rows;BenchmarkDiff*the frames of a window as godiff's, two lists of texts and rich texts, unchanged or scrolling, with their allocations (TestDiffFrameAllocslogs them). The GPU renderers' tests drawgputest.Sceneand compare it with the CPU renderer's drawing: on Windows in a hidden window, on macOS into an offscreen texture, on Linux into a framebuffer of a context EGL makes without a window (Mesa's surfaceless platform, llvmpipe without a GPU), once with OpenGL 3.3 and once with OpenGL ES 3.0.
A new widget composes elements (ui/widgets.go), keeps what it needs from
frame to frame with Local or in state, declares its interaction with
element flags (flagClickable, flagFocusable, …), paints in Draw, and
gets a test with Tester. A new GPU renderer draws the instances of
gpu.Builder with a port of the shader, syncs atlases with
Atlas.Changes, is created in ui/gpu_<os>.go behind gpuRenderer, and
has a test that compares its drawing of gputest.Scene with the CPU
renderer's (gputest.Compare).
Native modules (native, internal/nativegen)
A native module (the guide) is a Go package whose
interfaces Swift, Rust or Zig implement, each in a directory named after
the language (swift/, rust/, zig/), all behind one C ABI. Three parts
make it work:
internal/nativegenreads the package withgo/astalone (no type checking, so it is fast and needs no dependency built): the interfaces marked//mygo:nativeand everything they reach, structs, enums (a named string or integer type and its constants, read from literals and simpleiotaexpressions) and the errors made withnative.NewError. What crosses is a closed set of types (Type.Kind); anything else is aDiagat its line, with what to write instead. An interface the module returns is the platform's, one it only takes is Go's (ByGo). It writes<package>_native.go(constructors and proxies,gogen.go) and<Module>.mygo.swift(protocols, structs, enums,<Module>Error, the@_cdeclentry points and the glue insupport.go), and on both sides the functions that encode the composite types the methods cross (wire.go:_enc_<T>/_dec_<T>in Go,_mygoWrite_<T>/_mygoRead_<T>in Swift,<T>named after the type, asslice_map_int, and a type of another module, whose struct fields name its types relative to it, as<Module>_<T>). Forrust/andzig/it writessrc/mygo.rs(rustgen.go: a trait per interface,Send + Sync, structs, enums,<Module>Error,#[no_mangle] extern "C"entry points, and the runtime ofrustsupport.go: threads for calls with a context and for streams,catch_unwindat every entry) orsrc/mygo.zig(ziggen.go: a struct of a pointer and a vtable per interface, whoseofchecks at compile time that the implementation has each method,exportentry points, an arena per call, and the runtime ofzigsupport.go), the library's root, which imports the implementation asimpl. Both refuse what only Swift implements (portable: views, textures, windows, other modules' interfaces, returned buffers). All carrymygo:native <version> <hash>, the hash of the package's type, const and error declarations with their comments, whichmygo buildcompares (Stale). The generated Go names its own identifiers with a leading underscore (_x,_ctx,_err,_r,_h,_fN,_gN, and the packages it calls, imported as_native,_runtime,_context,_fmt,_filepath), and the parser rejects parameters named so, so that no parameter clashes with them; the signatures keep the interface's names. An argument that stands for memory or an object of the platform's (a buffer, a module's object, a view) is kept reachable until the platform is done with it: the call returns, or, for a stream, the stream ends. A stream's arguments are made as its loop starts, and the callbacks and values of Go's among them forgotten if it does not start (native.Stream'sprepare). The scaffolds ofmygo native newandaddare inwrite.go.nativeis the runtime the generated Go calls, imported only by modules, so apps without one link none of it. It loadslib<Module>.dylibas packageffiloads any library (ffi.New(name, nil).From(dir):MYGO_NATIVE_<NAME>, the bundle'sContents/Frameworks, next to the executable, then.mygo/nativeof the module of the package's directory, whichruntime.Callerof the generated file finds in builds without-trimpath, go test and go run, checked against the sources' times of the module's languages (lib_ffi.go); on Linux and Windows the same, Rust and Zig everywhere and Swift on Linux for go test only (Module.supported).mygo_<m>_inittakes, beside the callback, the C runtime'smallocandfree(libc's, ucrtbase's on Windows), with which every implementation allocates the blocks it gives Go and frees Go's, whatever its own allocator. Every entry point has one C signature,(obj, args, len, call) -> block: arguments go in the binary encoding (codec.go: integers as 8 bytes little-endian, floats as float64, lengths before bytes and elements, a flag before a pointer's value, structs as their fields in order, an empty one as a zero byte so that every element takes one and a length is never more than the bytes left), from a pooledEncoder; results come back in a blockmalloced by the side that made it (status, length, bytes) and freed by the other, which Go'sDecoderreads in place and frees onClose. Errors cross as a kind, code, domain, number and message, both ways. A method of numbers and bools that returns no error (direct) has a_directentry point instead,(obj, a0, a1 …) -> UInt64, called with them as registers: integers as their bits, floats as a float64's, bools as 0 or 1. One purego callback, created once for all modules, carries everything the platform tells Go (kind*incall.go): results of asynchronous calls and values of streams, by call id; notifications and questions of callbacks, by the handle of a Go function; calls of methods of Go values, at once, or on a goroutine answered throughmygo_<m>_resume, by a continuation thatstatuscarries, so that Go answers it, with an error, even where it has no method to call; and releases. Continuations' ids are unique in the process: their high bits are the module's index (contBits), which Go gives each module as it loads it (mygo_<m>_init). A stream is pulled: Go callsmygo_<m>_nextfor each value, so a lentNativeBufferstays valid until the loop continues, and canceling the call cancels the Swift task. Objects areUnmanagedretains on the Swift side, released byCloseor aruntime.AddCleanup; objects of the main thread are released there, posted throughApp.WhenReady, never waited for on the runtime's one goroutine of cleanups. Notifications run in order on one goroutine, throughmygo.RunOnMainonce the app runs. Main-thread methods run throughRunOnMain, or directly on the main goroutine beforeApp.Run, and in Swift underMainActor.assumeIsolated.- The CLI (
cmd/mygo/native.go) finds the app's modules among the packages it is built from (a package with a generated_native.go), listed once per build and target with the native libraries' packages (Config.packageDirs), so that apps without a module list nothing more, regenerates them ingenerateanddevor refuses stale ones inbuild, runsswift build(debug for the host, or release for the target's architectures, both at once for universal), copies the dylib and the frameworks its package brings intoContents/Frameworks, whichcodesign --deepsigns, and merges the modules'Info.plistandEntitlements.plist(mergePlist) under the app's own keys, which settle the modules' conflicts. A module's language for a platform ismoduleLang(native_module.go): Swift on macOS when it hasswift/, else Rust, then Zig; Rust and Zig modules are libraries like those packages name, as resources (nativesOf,Config.nativePackagesadding each module's library to its package's manifest, with nobuild, asnativePackage.librarygives the language's per platform).mygo devwatches each language's directory and the plists, but not the generated files.mygo native callwrites a small typed program into the module's directory that decodes the arguments into the method's parameters, and runs it. - Views. An interface embedding
native.Viewis a view of the platform (anNSViewinswift/), its methods on the main thread; on platforms without an implementation its constructor returnserrors.ErrUnsupported, andui.NativeViewof nil is a plain box.native.Viewhas one method,NativeView() uintptr, the platform's view (0 once closed), whichui.PlatformViewasks for, so the public API names no internal type.ui.NativeViewisui.WebView's element around aplatformViewof the window's engine, which stands for the view as asurface.WebViewand has the surface host it as it hosts an embedded web view, throughviewhost.Embed, which package native sets as it initializes, so that only apps with a module link the hosting (platform.ViewSurface,EmbedView: awindowwhosewebis the view,foreign), so placing, clipping to the hole's corners, stacking, hiding and the pointer's way through the surface are the web view's. AplatformViewholds the view by its handle, never its Go value, so a view the app lets go of is released;nativetells the windows hosting a view as it closes or is released (viewGone, the functionEmbedwas given), which drop it (pruneViews). Closing one also takes theNSViewout of its superview, and a view another window took is put back as its window places it. Objects of the main thread close and release there. - Modules that use modules.
nativegenloads the native modules a module imports (go list -find), qualifies their types with their Swift modules, and makes generated declarationspublic. Generated packages register withnative.Register, which loads a module's dependencies before it (dyld then finds their libraries loaded, by install name, when it links them) and wraps another module's objects by name (Wrap). The CLI builds dependencies first and passes-I,-Land-lto the dependent'sswift build;.mygo/nativeis for editors only. - Textures.
native.Textureisinternal/texture.Texture.ui.Imagetakes it throughimagesrc.Frames, andui.ImageSourceis sealed byimagesrc.Mark, whichBitmap,SVGand textures embed, so thatuilinks no texture code. Swift presents anIOSurfacethrough the callback (kindPresent, the surface retained); the texture holds the surface presented last, the one shown and those of frames in flight (inFlight), each in use so that pixel buffer pools never hand them back to be drawn on screen, and asks the windows that drew it for a frame, those that drew it within the laststaleAfterpresents: the others no longer show it, until they draw it again. A frame'sscene.Imagewraps the shown surface: GPU renderers ask itsGPU, ascene.GPUSourcethe texture implements, for a texture of their API (scene.Metalon macOS, which wraps the surface with no copy), and draw its pixels, whichRestorereads, where it gives none, as the CPU renderer does, into a texture of their own: the Metal renderer marks the textures a source gave (texture.gpu) and never uploads pixels into one. - Published implementations (
native_swift.go,publishSwiftinnative_lib.go): a module is a native library of its own, named as its Swift module and built per platform in that platform's language (moduleTargets,moduleLibrary), somygo native buildpublishes its Rust and Zig builds as any library's, byGOOS-GOARCH, and its Swift as a universal release build packed as.tar.gzwith its frameworks, named after its SHA-256, uploaded withghto the module's releasenative-<name>of the checkout's repository unless-releaseor the library names another (uploadNatives, which never replaces a file: a name there already is the same bytes), recorded underdarwin-universalof the module'smygo-plugin.jsonwith the hash of its Swift package's sources, LF line endings, and those of the modules it uses (moduleSourceHash); it skips a module whose record matches, unless-force.swiftBuildtakes it while the hash matches, in release builds and wherever Swift is missing, through the cache of native libraries (fetchNative), unpacked with no path or link that leads out of its directory (untar).
CLI (cmd/mygo)
-
Browsers (
web.go):build -platform web(orjs/wasm) compiles the app withGOOS=js GOARCH=wasmand the production flags into<out>/web, with a page of its own (webPage: a spinner while the WebAssembly loads) and thewasm_exec.jsof the Go that compiled it, which must match.dev -webserves the page, the loader and the last build that compiled from memory (webDev), rebuilds on the watcher's changes as the desktop loop does, and tells pages through server-sent events (/__mygo/events):reloadafter a build,build-errorwith the compiler's output, which the page shows over the app until a build compiles. -
initrenderscmd/mygo/template: a Go module and a TypeScript frontend built with Vite, side by side at the project root like the examples, draws a default icon atresources/icon.png, fetches modules, installs the JavaScript dependencies with Bun and generates the client. package.json runs the CLI from mygo-cli (bun run dev,bun run build), or withgo run github.com/egoist/mygo/cmd/mygofor-mygo <checkout>, whose go.mod replaces the module with the checkout. mygo.config.ts importsdefineConfigfrom mygo-cli, or from the checkout'spackages/cliby a relative path between real locations, and setsdevUrl,devCommandandbuildCommand(thedev:webandbuild:webscripts, which run Vite and never mygo),frontendDist(Vite'sdist) andout(build, so the two do not meet);vite.config.tspins the dev server to the port ofdevUrland does not watch the development app and builds. Names reach the templates escaped for their language (json,printf "%q",html).-template nativerenderscmd/mygo/template/nativeinstead (the frontend's istemplate/web): an app of native UI with a test of its view and amygo.json, whose module has the CLI as a tool (go get -tool, or atoolline beside thereplaceof a checkout), forgo tool mygo devandbuild; no Bun. Without a frontend in the configuration,bindingsstays empty and the CLI writes no client. -
Both templates install the agent skills embedded under
cmd/mygo/template/shared/.agents/skills.install-skills [dir]refreshes the same bundled files in an existing directory, without reading project configuration or replacing unrelated skills and extra custom files. -
The configuration is
mygo.config.ts, ormygo.json(config.go,config_ts.go). For the former, Bun, else Node.js 22.6 or later (with--experimental-strip-typesbefore 22.18 and 23.6), runs a loader that imports it, awaits its default export or calls it with{ command }, and writes JSON to a temporary file; either way the JSON goes through the same checks. The loader goes to the runtime's standard input, never its command line: on Windows the runtime may be a batch file (npm'sbun.cmd), and cmd.exe cuts the command line at a line break. Errors name the file in use.defineConfigand the types of the configuration come frompackages/cli/index.d.ts;TestConfigTypeskeeps its interfaces in step with theConfigstruct. -
generatebuilds the app for the host and runs it in generate mode (MYGO_GENERATE;RequestSingleInstanceLockthen returns true at once). Before that, withi18nin the configuration, it checks the catalogs and writes their functions (see Catalogs of messages), asdevandbuilddo;i18n checkonly checks them.dev -langpasses the language to the app asMYGO_LOCALE(in browsers, throughgo.env). -
dev(dev.go,watch.go) writes the client asgeneratedoes (the frontend imports it), runsdevCommandin the project directory, waits fordevUrlto answer and runs a development build, pointed at it withMYGO_DEV_URL(or atfrontendDiston disk withoutdevUrl). The build is packaged like a release: on macOS a bundle named " Dev" with identifier ".dev" in.mygo/dev/<goos>-<goarch>, so bundle-only features (notifications, URL schemes) work and its data stays apart from the production app's. Builds are assembled in a staging directory and renamed into place, so the running build keeps its files.- Builds run with
MYGO_DEV=1, which the core takes out of its environment (dev.go):App.Relaunchthen exits with code 75, which hasmygo devstart the same build again, with the argumentsRelaunchwrote in the file ofMYGO_DEV_RELAUNCH. Production builds ignore it.-backgroundaddsMYGO_DEV_BACKGROUND=1, taken out the same way: the app does not come to the front as it launches (AppOptions.Background, macOS), and its windows show asShowInactivewould (backgroundShow) until a window takes the focus or the app becomes active. A build counts as started once its process is: one that crashes on start is reported as it exits. - Reload. A change (polling every 250 ms, debounced) rebuilds; when the executable, Info.plist and icon are unchanged nothing restarts. Otherwise the running build is sent SIGTERM (quit sequence), then SIGKILL after 3 s, and the new one starts once it has exited: builds never overlap, so the single-instance lock, the web view's profile and the app's files are free. Only the app gets the SIGTERM, which lets it end the processes it started; those left after it exits are killed. A build that fails to compile leaves the old one running; one that fails to start leaves none until the next change.
- Watching. Exactly what the build reads, from
go list -depsafter every build: the directories of the compiled packages outside GOROOT and the module cache (so localreplacemodules too), embedded files, go.mod/go.sum, the configuration, the icon and the resources, but not the platform directories of other platforms. Frontend sources are the dev server's business and never rebuild the app. A build keeps the watcher's baseline unless it changed what is watched, so edits made during a build trigger another one. On Windows the watcher opens directories sharing them for deletion (openDir), whichos.Opendoes not, so that deleting or renaming one it lists does not fail. - Quitting the app ends
mygo dev; a crash waits for the next change. Ctrl+C stops the app as a change does; a second one kills it and the dev server at once. The watcher reports the paths that changed, which the rebuild names. Lines typed on the terminal are shortcuts (rrestarts even an unchanged build,c,q,h); on Unix, mygo ignores SIGTTIN, so that reading them in the background fails rather than stopping it.
- Builds run with
-
Output (
console.go), on standard error: a line per step of a command. On a terminal (Windows: a console that takes VT sequences onceenableVTasks), the step in progress has a spinner, redrawn in place at 12 fps and cut to the width (termWidth, autowrap off besides), with its elapsed time and what it does:goBuildrunsgo build -vand shows the import paths it prints, and modules it downloads, keeping the other lines, the compiler's errors, for its error; downloads show their bytes. Steps nest (signing inside a build). What the programs the CLI runs print goes throughlabeledwriters, which erase the spinner's line, write whole lines with a label (web │,app │) and draw it again; the app writes to anos.Pipe, not a writer ofexec.Cmd, whoseWaitwould wait for the web view's processes holding it, and commands with writers get aWaitDelay. Without a terminal, each step has a start line and an end line, without colors unlessFORCE_COLOR(NO_COLORturns them off anyway).devandbuildopen with a banner whose five exclamation marks are those of MyGO!!!!!, in its members' colors, which light up one after the other on a terminal (redrawn two lines up with the cursor saved, until a line moves it). -
buildgenerates the client, runsbuildCommand, then compiles each target with-trimpath -ldflags "-s -w -X …production=1"(-H=windowsguion Windows) and-tags mygo_nodownload,mygo_noinspector, which leave out package ffi's downloader and the inspector of native UI (unlessMYGO_INSPECTOR=1), into a staging directory, so a failed build keeps the previous artifacts.frontendDistis embedded without touching the project (embed.go):go build -overlayadds a generatedmygo_frontend_gen.goto the main package, with//go:embed all:mygo_frontendand a call toSetFrontend, and maps everyfrontendDistfile into that virtual directory, so the frontend may live anywhere. macOS targets become.appbundles (Info.plist,.icnsrendered in pure Go, and withmacos.iconanAssets.car, as it is or compiled from an Icon Composer.iconbyxcrun actool), signed withmacos.signingIdentity(hardened runtime and timestamp for real identities, ad hoc by default);darwin/universalcombines both architectures with a pure-Go fat-binary writer. Linux gets a.desktopentry and icon. The production flag makesIsDevfalse, which disables the web inspector by default. -
Resources (
resources.go), as in quickgui: the contents of the project'sresources/directory, plus the files and directories listed inresourcesin the configuration under their base names, are copied intoContents/Resourcesof macOS bundles and next to the executable on Linux and Windows, bybuildanddevalike; apps find them withApp.Path(PathResources)(undergo run,./resources). The platform directories ofresources/, named<goos>or<goos>-<goarch>(platformDir; names other tools use, such asdarwin-x64, are errors), ship with the apps of that target only, their entries merged with the shared ones:mergergroups what goes to one path, ignoring case, and only descends into directories that several sources share, so a whole tree from one source stays one entry. Fordarwin/universal,darwin-arm64anddarwin-amd64are walked as pairs whose names must match: identical files and links ship once, an arm64 and an x86_64 Mach-O file become a universal binary (writeUniversal) when copied, anything else fails. A name starting with a dot is skipped when it is a directory or a file manager's dropping (.DS_Store,Thumbs.db,desktop.ini), and at the top level ofresources/whatever it is; a dotfile inside a resource directory ships (junkName,hiddenName), since packages read such names. So is whatresourceFilterexcludes fromresources/(but not from listed paths): Go source files, which only builds read, and the paths there that matchresourcesExclude(path.Matchsegments,**for any number of them), with what a matching directory holds; the merger's listings andresource.walk, which copies and fingerprints, apply it, whilemygo dev's watcher still sees those files, which a package may embed. The entries of these directories and listed paths are followed when they are links, links inside them are copied as links, permissions are kept. Installed paths must be unique ignoring case (listed resources never merge), and top-level names must not replace the packaging's own files (AppIcon.icns,Assets.car, the executable, the.desktopentry). Code among them is signed before the app (signNestedCode), sincecodesign --deeponly covers code directories and notarization rejects unsigned code: Mach-O files, then the bundles holding them, deepest first, as a bundle's signature seals what it holds (and signing its main executable signs the whole bundle). A real identity signs all of it, keeping the entitlements of each (--preserve-metadata) unlessmacos.helperEntitlementsnames others; ad hoc signing only signs code without anLC_CODE_SIGNATURE, and the bundles around it, so vendors' signatures stay. Windows builds sign the PE images among the resources that have no certificate table, with the app's certificate or command. The packages of the app may name native libraries in amygo-plugin.json(natives.go, the format ininternal/nativelib), with how to build each from its sources (build: a Rust crate for cargo, a Zig package for zig build) and its published files, one per platform with its URL and SHA-256, as the terminal plugin names libghostty-vt:go list -depsof the target finds them. A library withbuildin a package outside the module cache andvendor(the app's own, or a module replaced by a directory) whose sources are there is built for the target (nativebuild.go), unless its published file for the target was built from these sources (itssourceHash, the SHA-256 of the build directory's files butnativelib.Skippedones, with LF line endings, itsargsandmacOS) or the tool is missing, into.mygo/native/<goos>-<goarch>of its module, cargo withCARGO_TARGET_DIRand zig with--prefixand--cache-dirunder.mygo/nativetoo, so that builds write nothing into the sources but cargo'sCargo.lock. cargo-zigbuild, when installed with Zig, links Linux (against glibc 2.28, as Zig does) and other platforms' libraries but macOS's, which need Apple's SDK and so a Mac, and Windows' on Windows (MSVC). macOS libraries get the app's minimum version or the library'smacOS, the later (MACOSX_DEPLOYMENT_TARGET, Zig's-macos.<version>target, without which Zig asks for the latest macOS). The CLI empties the debug sections of the Linux libraries it builds (stripELF, asstrip --strip-debug), as Zig can keep them even whenbuild.zigasks to strip (libghostty-vt's). cargo's JSON messages name the crate's cdylib; zig's prefix is searched for the one library it installed. Both build incrementally, somygo devbuilds on every rebuild, and watches the sources (buttarget,zig-outand hidden directories). Other libraries are downloaded once into<user cache>/mygo/natives/<sha256>/, checking their SHA-256. Each is installed at the top of the resources like a listed resource, both architectures of a universal app combined.mygo native build(native.go) builds the libraries of a package for every platform (or-target's, adding to the files already named, skipping those whose file was built from the sources as they are unless-force, and listing the platforms that failed at the end) into.mygo/native/release, named<name>-<goos>-<goarch>-<sha256[:12]><ext>so that a release never replaces a file a published version of the package names, copies them into the cache, where apps on the machine find them before they are published, with-uploadputs them on a GitHub release withgh, not as the repository's latest, and then writes their entries intomygo-plugin.json. Packageffi(below) finds them at run time. The CLI and the plugins download into that cache withinternal/download: a 5xx, 408 or 429 response, or a connection that fails or breaks off, is tried again after 1, 2 and 4 s (GitHub's release downloads answer 502 and 504 now and then), and a lock on.lockin the file's directory (flock,LockFileEx) has processes that need the file at once download it once: the others wait, then find it. Without it each renamed its own copy onto the file, which Windows refuses ("Access is denied") while another process has it open or loaded.resources/icon.pngis the default icon. Each platform's output directory is assembled in a staging directory that replaces it whole, so removed resources do not linger; development builds on Linux and Windows remove what the previous build placed and this one lacks. -
Packages for the other platforms. Windows gets " Setup .exe", made with NSIS (
nsis.go): a per-user install in%LOCALAPPDATA%\Programs\<name>(windows.installDirNamenames the folder otherwise), where the updater can write and where the installers of later versions install too (InstallDirRegKeyreads the uninstall entry'sInstallLocation), a Start menu shortcut, a desktop shortcut that the finish page's second check box (MUI's "show readme" one) creates, and an uninstaller registered underHKCU\…\Uninstall\<identifier>;/S /D=<dir>installs silently, without the desktop shortcut. The uninstaller runs from a copy of itself that nothing waits for, so it removes the app's folder last: the folder is gone once the uninstall is done. It tries deleting each shortcut again for up to 5 s, as Explorer opens a new shortcut a few seconds after it appears, not sharing it for deletion, andDeletefails meanwhile.windows.installerIncludenames a script of the app's own that the generated one!includes after MUI2.nsh and the languages (nsisHooks); itscustomInit,customInstallandcustomUnInstallmacros, each inserted only under!ifmacrodef, run in.onInit, at the end of the install section and in the uninstall section beforeRMDir /r "$INSTDIR", as electron-builder's do; without it the script is unchanged.makensiscomes from an installation of NSIS or, on Windows, where NSIS is rarely installed, from the official zip of the release thatReleaseininternal/nsispins, which the CLI downloads once, checks against its SHA-256 and unpacks into<user cache>/mygo, as Tauri does. It tries the copies of the zip inRelease.URLsin order, until one answers with the right SHA-256: the asset of this repository'snsis-<version>release, then SourceForge, where NSIS publishes it. SourceForge's download host has been down for hours at a time, and its mirrors then redirect to it, so it is not the only source; a host that sends no response headers within 30 s gives way to the next. Updating NSIS means publishing the copy (see Releasing). Other systems skip the installer without NSIS: its zip holds Windows programs only. A signed app gets a signed uninstaller too, as with Tauri:!uninstfinalize(NSIS 3.08 and later) makes makensis runmygo sign-uninstalleron the uninstaller it generates, before it puts it into the installer, and the CLI signs it as it signs the app. The executable and the Windows configuration reach it through the environment (MYGO_SIGNER,MYGO_SIGN_SETTINGS), since NSIS reads$in the script as its own syntax. Linux gets a Debian package written in pure Go (deb.go), whose maintainer islinux.maintainer, else theauthorof package.json, else the name of the app: the app in/opt/<name>, a/usr/binlink for each name oflinux.command, the desktop entry (categories, comment, URL schemes;Execis the app's path) and hicolor icons, depending on GTK 3 and WebKitGTK 4.1. It also gets an RPM package (rpm.go), built withgo.digitalxero.dev/rpm, with the same files and metadata, and aPKGBUILD(pkgbuild.go), from whichmakepkg -sipackages the archive for Arch Linux (sourceis its file name, with its SHA-256): itspackage()installs what the Debian package holds, with the same modes, the desktop entry written in it withprintfasdeb.gorenders it, and the hicolor icons, which the build resizes as for the Debian package and writes next to it as more sources (<name>-256x256.png,<name>-512x512.png); pacman's hooks register them, the MIME package and the desktop entry, as the triggers of Debian do.pkgveris the version without the hyphen of a pre-release (1.2.0beta.1; vercmp sorts1.2.0_beta.1after1.2.0, as a remainder starting with a separator wins),dependstakes per-formatdeb,rpmandarchlists, or maps a list of Debian dependencies througharchPackagesandrpmPackages, warning about those they lack, andoptionskeeps the executable unstripped. Every value is quoted for the shell, and-uploadleaves it out (uploadstakes only known kinds).linux.formatschooses which of the three packages a build writes (writeLinuxPackages), all by default; the archive is always written. Packages hold the same files as the update archive; apps installed by a package manager do not update themselves (Updater.Enabledchecks that the app can write where it is installed). Every Linux build also getsinstall.sh(installscript.go), a POSIX sh script, the same for every architecture, that installs the archive for the user in~/.local/<name>.app(which updates can replace), links~/.local/bin/<command>for each name oflinux.commandthat no other program has (and removes a~/.local/bin/<name>link of its own, which earlier scripts made), and registers the app's<name>.desktop, with absoluteExecandIconpaths, and<name>.xml, the shared-mime-info package of the types it defines, under$XDG_DATA_HOME. It then warns whenldconfig -plists no WebKitGTK (4.1 or 4.0, the librariesinternal/linuxloads), with the command that installs it on the distribution/etc/os-releasenames, and stays silent when it cannot tell (noldconfig, or a cache without libc, as on NixOS or musl).Update.Installregisters them again from the new version (update.RefreshDesktopEntry, the same rewrite in Go, which a test compares with the script's) when the user's entry runs the app it updated. It takes the archive it is given, else the one of its version next to it, else, with updates, theurlof the target's manifest, read with sed from the indented JSONmygo buildwrites.--uninstallremoves only what points into its install, including the URL handler entry the app registers. -
On a macOS host, macOS targets also get "--.dmg", or "-.dmg" for darwin/universal (
dmg.go): the CLI puts a copy of the app (ditto --noclone, since a clone of a sparse file takes fewer blocks than its copy on HFS+ will), the/Applicationslink, the volume icon and a.DS_Storewritten in pure Go (dsstore.go, byte-identical to dmgbuild'sds_storepackage) that lays out the Finder window in a folder,hdiutilcreates a writable HFS+ image of it, as large as its files need (APFS where HFS+ fails, as on macOS 26.7.1), the CLI sets the volume's Finder flags, thenhdiutil convertcompresses it with LZMA. No AppleScript or Finder automation is involved, so it works headless and in CI. The image is signed with a real identity and, withmacos.notarize, notarized withnotarytooland stapled. -
keygenwrites the update signing keys (see Updates above).
Configuration lives in an optional mygo.config.ts or mygo.json
(cmd/mygo/config.go): app
metadata (the icon defaults to resources/icon.png), extra resources,
the frontend (devUrl, devCommand, buildCommand, frontendDist,
bindings) and the macos section (minimum system version, signing
identity, entitlements of the app and of helpers, DMG title, notarization
profile).
Platform differences
| feature | macOS | Linux | Windows |
|---|---|---|---|
| watching files | FSEvents, one stream per tree, no descriptor per file | inotify, per directory | ReadDirectoryChangesW, one for a recursive tree, and the parent to see the directory renamed |
| menu bar | application menu bar, default menu installed | per-window GTK menu bar, none by default | per-window Win32 menu bar, none by default |
| menu item icons | NSMenuItem image, 16 pt, template images tinted |
a GtkBox of a GtkImage (a cairo surface at the widget's scale) and the label; none on the bar's own items; tray menus' items get icon-data over DBusMenu (dbusmenu_gtk_parse_get_cached_item) |
Windows 11: drawn by MyGo in a column after the checks' (premultiplied BGRA, internal/menuicon); Windows 10: hbmpItem, a 32-bit premultiplied DIB section freed with its menu; none on the bar's own items |
| menu item subtitles | NSMenuItem subtitle from macOS 14; before, after the title (Label — Subtitle) |
after the label | after the label, with its ampersands escaped |
| auto-hide menu bar | ignored | the bar widget hides; can-activate-accel keeps its shortcuts; Alt alone or F10 show it and open its first menu until it deactivates |
the menu is attached only for the SC_KEYMENU menu loop that Alt alone or F10 start; shortcuts come from the webview |
| tray | NSStatusItem, click events (a Control-click a right-click) | AppIndicator: a menu, and activation (a double click) for an icon with an OnClick listener |
notification area icon, click events |
| global shortcuts | Carbon hot keys | X11: XGrabKey on the root window (with Caps/Num Lock variants), key presses from a GDK filter. Wayland: the XDG GlobalShortcuts portal (see Linux) |
RegisterHotKey |
| notifications | UserNotifications, packaged apps only; Group is the threadIdentifier; Image a UNNotificationAttachment of a temporary file named for its format, which the system moves into its store; optional Sender uses communication notifications with the signed capability; the delegate is attached at launch, for the click that launched the app |
org.freedesktop.Notifications over D-Bus; Image is the image-data hint, decoded by GdkPixbuf and fit to 256 pixels; ignores Group and Sender |
notification-area balloons (toasts); Image is the balloon's large icon (hBalloonIcon, NIIF_LARGE_ICON), decoded by WIC, twice SM_CXICON; ignores Group and Sender |
| notification removal | removeDeliveredNotificationsWithIdentifiers:; ClearNotifications removes all, earlier runs' too |
CloseNotification on the bus, for those of this run |
hides the balloon, which goes away by itself anyway |
a web view scaled and rounded with native UI (WebViewPlacement) |
the pane's bounds scale it, its layer's corners cut it | WebKitGTK zooms the page to its view's size, which resizes it at the same width; draw clipped to the corners on whole pixels |
the visual's transform scales it, a rectangle clip cuts it |
WebViewAvailable (any goroutine, before Run too) |
WKWebView is there, which macOS always has |
WebKitGTK 4.1 (or 4.0), JavaScriptCore and libsoup load, as webKit loads them for a window showing a page |
internal/webview2 finds a runtime, or a WebView2Loader.dll next to the executable exports what createEnvironment calls; once an environment was made, whether that worked. A runtime whose files are there but whose browser is not passes, and fails later, making the web view |
| secrets | Security.framework generic passwords in the login keychain (SecItem*), service and account as keytar |
libsecret's org.freedesktop.Secret.Generic schema (attributes service, account), loaded on first use, libsecret 0.19 or later; no libsecret, no session bus (g_bus_get_sync) or no Secret Service on it (G_DBUS_ERROR_SERVICE_UNKNOWN, NAME_HAS_NO_OWNER, NO_SERVER) is ErrUnsupported |
Credential Manager generic credentials (CredWriteW), target service/account |
| vibrancy | all materials, behind pages and native UI; Liquid Glass (NSGlassEffectView) from macOS 26, the popover and HUD materials before, with a tintColor and the native UI's Glass{Native} placed over web views; frameless windows cut to CornerRadius; SetActiveAppearance through AppKit's private _setForceMainAppearance: |
ignored | Windows 11 22H2 Mica, Acrylic, Tabbed, behind pages and native UI, in windows created with a material, which have no menu bar; Liquid Glass as Acrylic; frameless windows given a CornerRadius rounded by DWM |
| active windows | the core's state, from key windows; materials follow AppKit's (NSVisualEffectStateFollowsWindowActiveState), but panels', flyouts' and kept ones' (NSVisualEffectStateActive) |
the core's state, from GDK's focus; GTK's own title bar goes :backdrop as a focusable flyout takes the keyboard |
the core's state, from WM_ACTIVATE; DWM's Mica and Acrylic follow the frame, kept active while the window's focusable flyout is (WM_NCACTIVATE) |
| accent color and accessibility preferences | NSColor.controlAccentColor (10.14+), accessibilityDisplayShouldReduceMotion (10.12+), accessibilityDisplayShouldReduceTransparency and accessibilityDisplayShouldIncreaseContrast (10.10+), with NSSystemColorsDidChangeNotification and NSWorkspaceAccessibilityDisplayOptionsDidChangeNotification; no text scale |
GTK's gtk-enable-animations and theme name, and the settings portal's accent, contrast and GNOME's text-scaling-factor (ReadAll, SettingChanged); no reduced transparency; the pointing devices from the default seat's, as they come and go |
DWM's AccentColor, SPI_GETCLIENTAREAANIMATION, Personalize's EnableTransparency, SPI_GETHIGHCONTRAST and Accessibility's TextScaleFactor, with WM_SETTINGCHANGE; the pointing devices from SM_MOUSEPRESENT, SM_DIGITIZER and tablet mode, not followed as they are plugged in |
| preferred languages | the global AppleLanguages (CFPreferencesCopyValue), not the app's |
LANGUAGE, then LC_ALL, LC_MESSAGES, LANG; read when asked, no changes reported |
GetUserPreferredUILanguages, WM_SETTINGCHANGE "intl" |
| regional formats | autoupdatingCurrentLocale, its calendar's first weekday, the hour of the skeleton "j"; NSCurrentLocaleDidChangeNotification and System Settings' distributed notifications |
glibc's nl_langinfo_l of LC_TIME (_NL_TIME_FIRST_WEEKDAY, T_FMT), else by region |
GetLocaleInfoEx of the user's default: LOCALE_IFIRSTDAYOFWEEK, LOCALE_STIMEFORMAT |
| the app's language kept | AppleLanguages of the app's defaults |
the file locale in PathUserData |
the file locale in PathUserData, and the process's preferred UI languages |
| web views' language | from the app's AppleLanguages, next launch |
webkit_web_context_set_preferred_languages, live |
the environment's Language, as it is created |
Theme.SetSource |
NSApp's appearance (Aqua, DarkAqua, or none) |
gtk-application-prefer-dark-theme; light also puts a theme named for its dark variant (Yaru-dark) in place of its light one (Yaru), which GTK and WebKitGTK otherwise take for dark, and the other sources reset the theme. Without a source, or with system, the setting follows the settings portal's color-scheme (GNOME's and KDE's dark style, which GTK 3 does not read) from Init on: dark where the portal prefers dark, else the desktop's own GTK settings |
the title bars, menus and native UI, and pages through the DevTools protocol's emulated prefers-color-scheme |
Theme.SetGTKCSS |
nothing | a GtkCssProvider for the default screen at GTK_STYLE_PROVIDER_PRIORITY_USER, replaced by the next sheet that parses |
nothing |
a press the system cancels in native UI (InputEvent.Canceled, PressCanceled) |
a context menu shown while a button is down, whose loop takes the release | GDK_GRAB_BROKEN of the pointer, as a menu or another window takes the grab; a touch's GDK_TOUCH_CANCEL taps nothing |
WM_CAPTURECHANGED (and so WM_CANCELMODE) while a button is down; WM_POINTERUP with POINTER_MESSAGE_FLAG_CANCELED |
pointer device and pressure of native UI (InputEvent.Pointer, Pressure) |
a mouse event's subtype: a tablet's pen (NSEventSubtypeTabletPoint, with pressure, or TabletProximity) or a touch; tabletPoint: events, a pen pressing harder or softer between moves, are moves where the pointer is |
the event's source device (gdk_event_get_source_device): a pen or eraser, or a touchscreen, its pressure axis (gdk_event_get_axis); the area's own touches are fingers |
WM_POINTER messages, left to DefWindowProc to make mouse messages of, give the pressure (GetPointerPenInfo, GetPointerTouchInfo); the mouse messages' GetMessageExtraInfo (MI_WP_SIGNATURE) tells a pen's or a finger's |
| cursors of native UI without a system cursor | CursorWait, CursorHelp, CursorZoomIn, CursorZoomOut, CursorAllScroll and CursorCell from AppKit's private class methods (_waitCursor, _helpCursor, …, looked up with respondsToSelector:), CursorProgress its busyButClickableCursor, else the nearest public cursor or the arrow |
the CSS names from the cursor theme, then GDK's X cursors, then the nearest (cursorFallbacks) |
CursorZoomIn, CursorZoomOut and CursorCell drawn (CreateCursor, cursorMasks), the others IDC_APPSTARTING, IDC_WAIT, IDC_HELP, IDC_SIZEALL, IDC_NO |
| traffic lights, Dock | yes | ignored | ignored |
| hidden title bar | AppKit's traffic lights over a full-size content view | GTK's title buttons in header bars over the page, per gtk-decoration-layout; none where the Wayland compositor decorates windows |
caption buttons drawn in a layered child window, through DirectComposition over a material, light or dark per window (SetWindowButtonsTheme); snap layouts; a top edge that resizes |
| progress bar | Dock tile content view (NSBoxes: NSProgressIndicator does not draw there), app-wide | Unity launcher API over D-Bus (com.canonical.Unity.LauncherEntry), app-wide |
ITaskbarList3, per window |
| file icons | NSWorkspace iconForFile: drawn into an sRGB NSBitmapImageRep at the size |
GIO's standard::icon, or a desktop entry's GAppInfo icon, through gtk_icon_theme_lookup_by_gicon |
IShellItemImageFactory::GetImage (SIIGBF_ICONONLY), a DIB with straight alpha |
icons of file types (TypeIcon) |
UTType typeWithFilenameExtension: → NSWorkspace iconForContentType: (iconForFileType: before macOS 11), drawn as file icons |
g_content_type_guess of a name with the extension → g_content_type_get_icon, through the icon theme |
the image factory of a simple ID list (SHSimpleIDListFromPath), which needs no file, as file icons |
| installed apps | .app bundles of the Applications folders, NSFileManager displayNameAtPath: and NSBundle, off the main thread; launched with openURL: |
g_app_info_get_all filtered by g_app_info_should_show; launched with g_app_info_launch and GDK's launch context |
FOLDERID_AppsFolder enumerated in an STA of its own; launched with ShellExecute of shell:AppsFolder\<ID> |
| badge count | Dock tile label | Unity launcher API count | not shown |
| skip taskbar | ignored | skip-taskbar hint | ITaskbarList::DeleteTab (the window style is untouched) |
| FlashFrame | informational Dock bounce | urgency hint | FlashWindowEx until focused |
| visible on all workspaces | NSWindowCollectionBehaviorCanJoinAllSpaces |
gtk_window_stick |
ignored |
| window icon | ignored | gtk_window_set_icon |
WM_SETICON at the window's DPI |
| URL schemes | Info.plist (urlSchemes); RegisterURLScheme makes the app the default handler |
desktop entry + mimeapps.list |
HKCU\Software\Classes |
| downloads | shouldPerformDownload, non-displayable or attachment responses → WKDownload delegate |
download-started / decide-destination on the web context; response policy for attachments |
DownloadStarting (ICoreWebView2_4), replacing WebView2's download UI |
| Cookies | the session's WKWebsiteDataStore's WKHTTPCookieStore, URLs matched in Go (platform.CookieMatchesURL), Set through NSHTTPCookie's parser with a stand-in name and value; Clear with removeDataOfTypes: |
the session's web context's WebKitCookieManager (2.42+; get_cookies for a URL), kept in cookies.sqlite of the user data directory, or of a persistent session's directory (set_persistent_storage, as WebKitGTK keeps them in memory otherwise); libsoup 3 or 2.4 to match WebKitGTK 4.1 or 4.0; Clear with webkit_website_data_manager_clear |
ICoreWebView2CookieManager (ICoreWebView2_2) of a web view of the session's profile showing a page, a window's or one in native UI, or while there is none of a hidden web view of the profile in the app window, closed once one shows a page, once an ephemeral session's store closes, or once no call needed it for 5 s and closing it loses no cookie (a GetCookies finds no session cookie, nor any of an InPrivate profile, which go with WebView2's processes); a window's first navigation waits for the calls under way; ClearBrowsingData goes the same way; changes are polled with GetCookies until they show, failing after 2 s; GetCookies(uri), DeleteAllCookies; put_Expires called with comCallFloat |
| content rules | the default WKContentRuleListStore, which keeps compiled lists under an identifier of the id and a hash of the rules (looked up first, the id's others removed), on each window's WKUserContentController; the store's reason for a failure is in NSHelpAnchorErrorKey |
a WebKitUserContentFilterStore in content-rules of the user data directory (2.24+), on each window's WebKitUserContentManager |
matched in Go (internal/contentrules, indexed by the domains of url-filters and four bytes of their literals) from WebResourceRequested with a * filter, blocked pages answered with an empty 403 and other blocked requests with a redirect to data:, which Chromium fails as a network error; WebSocket handshakes never reach it; css-display-none rules as a style sheet a document-creation script adopts, kept when the page sets adoptedStyleSheets; parsed on a goroutine, nothing kept |
FitContent |
the bridge, as elsewhere; WebKit's private _setSizeToContentAutoSizeMaximumSize: is not used; native UI measures itself (ui/fit.go) |
the bridge; native UI measures itself | the bridge; native UI measures itself |
Page.ThemeColor |
the bridge, as elsewhere; WKWebView's themeColor (macOS 12+) is not used |
the bridge | the bridge |
Page.ProcessID |
the web content process, -[WKWebView _webProcessIdentifier] (private, looked up with respondsToSelector:) |
0: WebKitGTK has no API for it | the browser process the app's web views share, ICoreWebView2::get_BrowserProcessId |
Page.GPUProcessID |
WebKit's GPU process, which plays the page's sound, -[WKWebView _gpuProcessIdentifier] (private, looked up with respondsToSelector:); 0 until WebKit starts it |
0: WebKitGTK has no API for it | the GPU process of the environment, the COREWEBVIEW2_PROCESS_KIND_GPU entry of ICoreWebView2Environment8's GetProcessInfos, queried for each call; 0 on runtimes without it |
a page's sound (IsAudible, SetAudioMuted) |
private -[WKWebView _isPlayingAudio], key-value observed, and _mediaMutedState / _setPageMuted: with the _WKMediaAudioMuted bit, the capture bits kept; looked up with respondsToSelector:, not audible and not muted where WebKit doesn't answer |
webkit_web_view_is_playing_audio and notify::is-playing-audio; webkit_web_view_set_is_muted / get_is_muted (2.30+, bound when present) |
ICoreWebView2_8's IsDocumentPlayingAudio, IsDocumentPlayingAudioChanged and IsMuted, queried for each call; a mute set before the web view is made is put in setUp, before its first navigation |
BackgroundThrottling: ThrottlingOff |
inactiveSchedulingPolicy WKInactiveSchedulingPolicyNone (macOS 14+) and the private _hiddenPageDOMTimerThrottlingEnabled, _hiddenPageCSSAnimationSuspensionEnabled, _pageVisibilityBasedProcessSuppressionEnabled off, on a copy of the preferences when window.open() handed over the opener's; the private -[WKWebView _setWindowOcclusionDetectionEnabled:NO]; all looked up with respondsToSelector: |
the web view's settings' HiddenPageDOMTimerThrottling and HiddenPageCSSAnimationSuspension features off (webkit_settings_set_feature_enabled, 2.42+, bound on first use); hidden and minimized pages still stop drawing |
--disable-background-timer-throttling --disable-renderer-backgrounding --disable-backgrounding-occluded-windows in the environment options' AdditionalBrowserArguments when the page that starts the environment asks (later ones are logged); an embedded web view's controller still goes invisible with its host, which takes its page off the desktop |
a page's capture (CaptureState, SetCaptureState) |
WKWebView's cameraCaptureState and microphoneCaptureState (macOS 12+) and private _displayCaptureState (macOS 13+), key-value observed, and their setters set…CaptureState:completionHandler: (nil handler), looked up with respondsToSelector:; SetAudioMuted's _setPageMuted: has one bit for the camera and the microphone, so with only one of them muted the other is unmuted again after it |
webkit_web_view_get_camera_capture_state and its microphone and display siblings with their notify:: signals and setters (2.34+, bound when present); webkit_web_view_set_is_muted drops the capture mutes, which SetAudioMuted sets again |
none: CaptureNone, setting does nothing |
Autoplay: AutoplayAllow |
WKWebViewConfiguration's mediaTypesRequiringUserActionForPlayback WKAudiovisualMediaTypeNone; a window.open() window that doesn't ask gets the default of a new configuration back on the opener's copy |
webkit_policy_decision_use_with_policies on every main-frame navigation, with webkit_website_policies_new_with_policies autoplay WEBKIT_AUTOPLAY_ALLOW (2.30+, bound when present; its variadic name/value pairs bound for one pair, as g_object_set): WebKitGTK decides autoplay from the navigation's policies, WEBKIT_AUTOPLAY_ALLOW_WITHOUT_SOUND by default, which media-playback-requires-user-gesture (off by default) does not lift |
--autoplay-policy=no-user-gesture-required in the environment options' AdditionalBrowserArguments when the page that starts the environment asks (later ones are logged); then every page plays without a gesture |
picture in picture (PageOptions.PictureInPicture) |
private -[WKPreferences _setAllowsPictureInPictureMediaPlayback:]; -[WKWebView _canTogglePictureInPicture], _togglePictureInPicture and _isPictureInPictureActive, looked up with respondsToSelector:; a main-frame navigation leaving with a video out turns _setUsesPageCache: off until one leaves without, as WebKit restores such a page believing its video still out |
none: TogglePictureInPicture does nothing, IsPictureInPictureActive is false |
Chromium's, on whatever the option; TogglePictureInPicture runs requestPictureInPicture() (or exitPictureInPicture()) through the DevTools protocol's Runtime.evaluate with userGesture, IsPictureInPictureActive evaluates document.pictureInPictureElement |
a page's session state (SessionState) |
WKWebView's interactionState (macOS 12+, looked up with respondsToSelector:), NSData of WebKit's _sessionStateData; restored when the back/forward list then has a current item; WebKit puts a page that scrolled before its window first showed on screen back at the top as the window does, so a page restored before then gets a script in the default client world, as it loads, that puts it back at the private _scrollPosition of the current item when it next scrolls to the top before any input |
webkit_web_view_get_session_state / _session_state_serialize, _session_state_new / restore_session_state, then go_to_back_forward_list_item of the current item (2.12+, bound on first use) |
none: the core keeps the URL alone and loads it |
ClearBrowsingData, Session.ClearData |
the session's WKWebsiteDataStore, allWebsiteDataTypes or the WKWebsiteDataTypes of the kinds the system's WebKit exports |
the session's web context's website data manager, WEBKIT_WEBSITE_DATA_ALL or the kinds' types |
the WebView2 profile's ClearBrowsingDataAll, or ClearBrowsingData of the kinds' COREWEBVIEW2_BROWSING_DATA_KINDS (ICoreWebView2Profile2, through the web view cookie calls use) |
sessions (PageOptions.Session) |
a WKWebsiteDataStore set on each web view's configuration: dataStoreForIdentifier: of a UUID made of the name (macOS 14+; before, nonPersistentDataStore kept until the app quits), nonPersistentDataStore for an ephemeral one, released when its last page closes; window.open hands the opener's configuration over |
a WebKitWebContext per session, made with webkit_web_context_new_with_website_data_manager of a manager with base data and cache directories in Sessions/<UUID> of the user data and cache directories, or webkit_website_data_manager_new_ephemeral; web views made with g_object_new and web-context, which keeps a reference to it; custom schemes, languages and downloads set up on each context; unreferenced when an ephemeral one's last page closes |
a WebView2 profile per session (ICoreWebView2Environment10's controller options: put_ProfileName mygo-<UUID>, or mygo-private-<n> with put_IsInPrivateModeEnabled for an ephemeral one, a new name for each of its stores); windows, web views in native UI and the cookie calls' hidden web views made with those options |
HTTP sign-in (SetAuthHandler) |
webView:didReceiveAuthenticationChallenge:completionHandler:, NSURLCredential for the session; none: no prompt, the 401 page; empty answer: RejectProtectionSpace |
WebKitWebView::authenticate, webkit_credential_new for the session; none: WebKitGTK's dialog; empty answer: authenticates without credentials |
ICoreWebView2_10's BasicAuthenticationRequested, scheme and realm parsed from its challenge, no proxy flag; none: WebView2's dialog; empty answer: Cancel |
certificate errors (SetCertificateErrorHandler) |
the server trust challenge of every connection, checked with SecTrustEvaluateAsyncWithError while a handler is set, allowed with credentialForTrust: for that connection |
load-failed-with-tls-errors, the main frame only; allowed with webkit_web_context_allow_tls_certificate_for_host (app-wide) and loaded again |
ICoreWebView2_14's ServerCertificateErrorDetected, every request, after NavigationCompleted failed; allowed with ALWAYS_ALLOW (app-wide) |
client certificates (SetClientCertificateHandler) |
the client certificate challenge, identities of the keychain (SecItemCopyMatching, kSecClassIdentity), which the keychain asks the user for, answered with credentialWithIdentity:certificates:persistence:; none: WebKit never asks; empty answer: reject |
scheme CLIENT_CERTIFICATE_REQUESTED on WebKitWebView::authenticate; WebKitGTK lists no certificates, so none are offered and a handler can only cancel (webkit_authentication_request_cancel), which ends the load as a cancellation OnDidFailLoad does not report; none: WebKitGTK's certificate chooser |
ICoreWebView2_5's ClientCertificateRequested, the mutually trusted certificates; none: WebView2's dialog; empty answer: the challenge is canceled, as a user's dismissal of the dialog is; a runtime without _5 never asks |
a page's context menu (OnContextMenu) |
-[NSView willOpenMenu:withEvent:] of MyGoWebView, on a menu with an item of forwardContextMenuAction: or a WKMenuItemIdentifier*: items named by their identifier, else WebKit's tag or AppKit's action, emptied and filled again in place; the target from the bridge alone, matched by the event's point; the app's items are buildItem's, sent to mygoContextItemClicked: and the menu's window |
WebKitWebView::context-menu: items named by their stock action, labels from their GtkAction, state from their GAction; the hit test tells all but the selection; renamed stock items are new ones of the same action; the app's items wrap a GSimpleAction (webkit_context_menu_item_new_from_gaction) |
ICoreWebView2_11's ContextMenuRequested, with the whole target (Complete); a changed menu is Handled, built by buildMenu and tracked after a deferral, and a WebView2 item chosen is its SelectedCommandId; a runtime without _11 never asks |
| permissions | WKUIDelegate media capture (camera, microphone) |
permission-request (camera, microphone, geolocation, notifications) |
PermissionRequested (the same four; WebView2 asks about others) |
| user agent | customUserAgent; UserAgentSuffix is the configuration's applicationNameForUserAgent, which a window.open() window does not inherit |
WebKitSettings:user-agent; UserAgentSuffix appends application details, split at the first "/" |
ICoreWebView2Settings2's user agent, and a suffix appended to the settings' own user agent; a runtime without Settings2 ignores both |
swipes and pinches (SwipeNavigation, PinchZoom) |
allowsBackForwardNavigationGestures and allowsMagnification of the WKWebView, both off unless asked for |
WebKitSettings:enable-back-forward-navigation-gestures; WebKitGTK has no setting for pinches, so the web view's GtkGestureZoom, which it keeps in its data as wk-view-zoom-gesture (2.38+), is given GTK_PHASE_NONE unless PinchZoom: it then sees neither a touchpad's pinch nor a touchscreen's |
ICoreWebView2Settings6's IsSwipeNavigationEnabled and ICoreWebView2Settings5's IsPinchZoomEnabled, both on in WebView2 and put to the options' values in setUp, before the first navigation |
| the browser's keys (reload, print, find, zoom) | none | none | WebView2's (F5, Ctrl+R, Ctrl+P, Ctrl+F, Ctrl+Plus, ...) only with the inspector (ICoreWebView2Settings3's AreBrowserAcceleratorKeysEnabled); AcceleratorKeyPressed still gives the menus every key |
| file associations | CFBundleDocumentTypes; files arrive with application:openURLs: |
desktop entry MimeType (%U), a shared-mime-info package in the .deb and .rpm for types the app defines (x-) |
ProgIDs and OpenWithProgids written by the installer |
| document windows | representedURL (proxy icon, Command-click folder menu) and setDocumentEdited: (the dot), then the traffic lights are laid out again |
the core puts * before the title |
the core puts * before the title |
| replacing a document's file | -[NSFileManager replaceItemAtURL:withItemAtURL:…] (permissions, dates, extended attributes, ACLs, identity) |
rename after chmod and lchown to the target's |
ReplaceFileW, retried on sharing violations; when it stops halfway, having removed the target, the replacement is renamed into place |
| recent documents | noteNewRecentDocumentURL: / clearRecentDocuments: of the shared NSDocumentController (the Dock menu) |
gtk_recent_manager_add_item, removing the items of g_get_application_name() |
SHAddToRecentDocs (SHARD_PATHW), IApplicationDestinations::RemoveAllDestinations; the process keeps the AppUserModelID the shell derives from its path, which the installer's shortcuts have too |
| jump list | ignored | ignored | ICustomDestinationList, committed whole from what App.JumpList keeps: the Recent category, AddUserTasks with an IShellLinkW per task (PKEY_Title, description, working directory, arguments quoted with syscall.EscapeArg), CommitList |
| Dock menu | applicationDockMenu: |
ignored | ignored |
| PrintToPDF | printOperationWithPrintInfo: save job (NSJobSavingURL), fit to width; the core rewrites the radicals the text names to the ideographs the fonts draw (pdf_text.go) |
WebKitPrintOperation to GTK's "Print to File" |
DevTools Page.printToPDF |
| power events | NSWorkspace sleep/wake, com.apple.screenIsLocked distributed notifications |
logind PrepareForSleep (system bus), screen saver ActiveChanged (GNOME, freedesktop) |
WM_POWERBROADCAST, WM_WTSSESSION_CHANGE |
| Now Playing and media keys | MPNowPlayingInfoCenter (its nowPlayingInfo and playbackState; artwork as an MPMediaItemArtwork decoding the bytes it holds when asked) and the targets of MPRemoteCommandCenter's commands, enabled as the info offers them; MediaPlayer.framework loads with the first Set |
an MPRIS player (org.mpris.MediaPlayer2 and .Player at /org/mpris/MediaPlayer2, exported with g_dbus_connection_register_object), whose bus name org.mpris.MediaPlayer2.<app>.instance<pid> is owned only while it shows something; PropertiesChanged on each change, Seeked when the position jumps; the artwork goes to a file in $XDG_RUNTIME_DIR for mpris:artUrl |
the SystemMediaTransportControls of the application window (ISystemMediaTransportControlsInterop::GetForWindow), with agile event handlers, a RandomAccessStreamReference over a memory stream for the thumbnail, and the timeline properties for position and seeking |
| KeepAwake | NSProcessInfo activity (shows in pmset -g assertions) |
XDG portal Inhibit, else org.freedesktop.ScreenSaver.Inhibit |
PowerCreateRequest |
| KeepRunning | NSProcessInfo activity NSActivityUserInitiatedAllowingIdleSystemSleep: no App Nap, idle sleep allowed (not in pmset -g assertions) |
nothing: desktops don't slow apps out of sight | nothing yet (power throttling, EcoQoS, would be SetProcessInformation's ProcessPowerThrottling) |
| IsOnBattery, IdleTime | IOKit power sources, CGEventSourceSecondsSinceLastEventType |
/sys/class/power_supply; Mutter idle monitor or GetSessionIdleTime |
GetSystemPowerStatus, GetLastInputInfo |
| network status | nw_path_monitor on a queue of its own, the first Status waiting (up to a second) for its first path; online when the path is satisfied, metered when it is expensive or constrained |
GNetworkMonitor, network-available (a default route) and network-metered |
GetNetworkConnectivityHint, NotifyNetworkConnectivityHintChange; online from local access up (not none or hidden), metered for a fixed or variable cost, roaming, over or near the data limit |
| window position | honored | ignored by Wayland compositors | honored |
| resize borders without a title bar | the window's own | the outer 5 px of the page | invisible, outside the window; along the top of a frameless window or a hidden title bar, a child window |
| content protection | yes | ignored | yes |
click-through (SetIgnoreMouseEvents) |
ignoresMouseEvents |
an empty input shape | layered, WS_EX_TRANSPARENT |
input region (SetInputRegion) |
ignoresMouseEvents switched as the pointer comes into the region or leaves it, from a global and a local monitor of its moves and releases, while a window has one, and a tracking area active always (input_region.go) |
the input shape, after GTK's own, on size-allocate (gdk_window_input_shape_combine_region) |
layered, WS_EX_TRANSPARENT switched as a timer at each tick of the system's clock (15.6 ms) finds the cursor in the region or out of it (input_region.go) |
| panels | NSPanel with the nonactivating style, floating, on all Spaces as a full-screen auxiliary |
sticky utility window, kept above, out of the taskbar and the pager; a gtk-layer-shell overlay surface on Wayland where it can be | topmost WS_EX_TOOLWINDOW owned by the hidden application window, rounded |
| flyouts | borderless nonactivating NSPanel child window, or an NSPopover; placed in the visible frame |
GTK_WINDOW_POPUP (override-redirect, placed in the work area, on X11; xdg_popup with gdk_window_move_to_rect on Wayland); focusable ones grab the seat |
owned WS_POPUP + WS_EX_TOOLWINDOW, WS_EX_NOACTIVATE unless focusable, CS_DROPSHADOW for a shadow, with corners rounded by DWM on Windows 11 |
| custom scheme origin | <scheme>://<host> |
<scheme>://<host> |
http://<scheme>.localhost, or http://<host>.<scheme>.localhost for another host (the page's location) |
LoadHTML documents |
a reload loads the base URL (about:blank without one), so the core cancels it (Navigation.Reload) and loads the HTML again; not in the history |
as macOS | as macOS, but in the history: going back to one with a base URL loads that URL |
| window.open | keeps the opener | independent window | keeps the opener: the new window's web view, set up, becomes the popup (put_NewWindow, under a deferral) |
keys held (Context.KeyDown) |
keyDown: and keyUp:; a local monitor of key ups hands the surface those let go of while Command is held, which sendEvent: keeps from the key window; resigning first responder or key window lets go of all |
key presses and releases, and a release the input method filters (HeldKeys.Lift): IBus and Fcitx hand back those they leave, which HeldKeys keeps from the content |
WM_KEYUP and WM_SYSKEYUP; a VK_PROCESSKEY release lets go of the key at its scancode; WM_ENTERMENULOOP lets go of all (HeldKeys.LiftAll), whose releases the menu's loop takes; WM_KILLFOCUS all |
| native UI surface | layer-backed NSView, frames from CADisplayLink (a timer at the display's rate before macOS 14), input methods through NSTextInputClient |
GtkGLArea (GtkDrawingArea without a GPU), GtkIMMulticontext | MyGoSurface child window, IMM32 |
| native UI data drags | NSDraggingSession, NSPasteboardItemDataProvider, NSDraggingDestination | GTK drag contexts, MIME selections, text/uri-list | OLE IDataObject, IDropSource, IDropTarget, Shell drag images |
concealed clipboard writes (ClipboardOptions.Concealed) |
org.nspasteboard.ConcealedType on every item, prepareForNewContentsWithOptions: with NSPasteboardContentsCurrentHostOnly |
the x-kde-passwordManagerHint target, secret |
the ExcludeClipboardContentFromMonitorProcessing format |
| native UI accessibility | NSAccessibilityElement subclasses |
ATK objects (GObject types registered through purego), bridged to AT-SPI by GTK | UI Automation fragments (COM objects; assembly thunks for the methods taking doubles) |
| native UI rendering | Metal, into a CAMetalLayer presenting as soon as drawn, or with the Core Animation transaction for frames that resize it or show web views | OpenGL 3.3 or ES 3.0 in the GtkGLArea's render signal; on the CPU, painted with cairo, where OpenGL runs on the CPU | Direct3D 11 (WARP without a GPU), flip-model swap chain presenting dirty rectangles |
| web views in native UI | WKWebView in a clip view under the surface; hitTest: lets the pointer through |
WebKitWebView in a GtkLayout under the area in a GtkOverlay; an input shape lets the pointer through |
WebView2 composition controller in a DirectComposition target under the frames'; the surface sends it the mouse |
| audio output | Audio Queue on the default output | PulseAudio simple API (PipeWire too), else ALSA default |
WASAPI shared mode, reopened when the default output changes |
| gamepads | GameController.framework, extended gamepad profile | evdev, the kernel's gamepad layout, Xbox's X and Y swapped back | XInput, four slots, Guide by XInputGetStateEx |