Skip to content

Debugging

mike-ward edited this page Sep 6, 2026 · 2 revisions

Debugging

A few widget mistakes are silent by construction because they produce no error.

  • Two widgets sharing an ID. ID is the identity key for focus, scroll offsets, and per-widget state, so the two collapse onto one identity.
  • A focusable widget with no ID. It renders and it clicks, but focus traversal is keyed by ID, so it never joins the tab order.
  • A scrollable widget with no ID. Every ID-less scrollable in a window shares the key "", so they scroll in lockstep.
  • An OnMouseLeave on a widget with no ID. Leave tracking is keyed by ID, so the callback never fires. This one survives FocusDisabled: true — opting out of focus does not opt out of needing an identity.

gui.Debug(true) — or GOGUI_DEBUG=1 in the environment — audits every frame for these and more, and writes findings to stderr, once per finding per window. gui.DebugCategories enables each class independently:

gui.Debug(true)
// gui: focusable shape at 0/2/1 has no ID; focus traversal is keyed by
// ID, so it renders and clicks but never joins the tab order

Leave it off in production: the checks walk the whole layout tree each frame and allocate while doing it.

For the mistakes that are visible in the source, requiredid reports them at build time instead, naming the Cfg type:

go run github.com/go-gui-org/go-gui/tools/requiredid/cmd/requiredid ./...

go vet -vettool= and a golangci-lint custom plugin work equally well. The tool is offered, not required — it is an internal tool whose rules may tighten between releases, so nothing breaks if you never run it. Without it, a widget that needs an ID and has none panics on its first render rather than failing your build.

Widgets that require an ID

Every input control panics when constructed without a non-empty Cfg.ID: Button, Input, InputDate, NumericInput, RadioButtonGroup, Radio, Select, Switch, Toggle, plus the stateful widgets that already did (ColorPicker, Combobox, DatePicker, ListBox, Slider, Tree, Table, Form, Menu, Menubar, ContextMenu, CommandPalette, ProgressBar, DataGrid).

The ID is what focus traversal, per-widget input state, scroll offsets, and OnMouseLeave dispatch are all keyed by, so a control without one is not merely anonymous — it is unreachable by keyboard and shares state with every other ID-less control. IDs must be unique within a window.

A decorative control that should never take focus opts out instead of inventing an ID:

gui.Button(gui.ButtonCfg{FocusDisabled: true, Disabled: true})

FocusDisabled: true satisfies the requirement for the widgets above. It does not exempt a widget whose ID is tagged gui:"required" without the focus option — those key state by ID regardless of focus.

Full finding list

Beyond duplicates and missing IDs, the sweep reports:

  • a scrollable container or OnMouseLeave shape without an ID
  • a virtualized listbox that resolved to height 0
  • a container setting both Wrap and Overflow (wrap wins)
  • a fill gradient with more stops than the GPU shader limit (silently resampled)
  • a window-level feature the platform refused (no ARGB visual, no compositor, refused opacity) — DebugWindowDegraded
  • per-widget state stored under a bare leaf instead of the effective ID — DebugUnresolvedKeys
  • a focus ID naming no widget in the frame — DebugUnknownFocus
  • a shape whose stamp disagrees with its scope, or an ID-bearing shape with no stamp (a hand-built Layout spliced into a generated tree) — DebugStampDrift
  • a callback that acted without ctx.Consume() while an ancestor also handles, and a link activation that opened nothing

DebugUnscopedIDs is separate and opt-in (not part of DebugAll): an ID with no ID-bearing ancestor, which cannot move into a second scope safely.

Assertable forms

Two methods return the audit findings as data, for tests and CI:

  • TestDuplicateIDs() — renders the window and returns duplicate-ID and missing-ID findings as strings. The assertable form of the stderr audit.
  • TestUnconsumedEvents() — dispatches one synthetic event per consume-class callback and returns the findings.
  • TestFindings(mask) — returns findings for any category mask, including opt-in ones.

See Testing your app for how NewTestWindow drives these.

Clone this wiki locally