gum-runtime-variable-references
Skill655 starsChanged 2 months agoMITView on GitHub
Runtime variable reference propagation and the optional Gum.Expressions NuGet. Triggers: ApplyAllVariableReferences, GumExpressionService, runtime styling/theming, GumExpressions project.
In this repository68 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- debuggingSkill
- frb-build-verificationSkill
- frb-pixel-harnessSkill
- gum-architecture-layersSkill
- gum-bundleSkill
- gum-cliSkill
- gum-cross-platform-unificationSkill
- gum-docs-writingSkill
- gum-file-pathsSkill
- gum-forms-behaviorsSkill
- gum-forms-controlsSkill
- gum-forms-default-visualsSkill
- gum-forms-itemscontrolSkill
- gum-forms-theme-importSkill
- gum-iconsSkill
- gum-issue-creationSkill
- gum-kernsmith-integrationsSkill
- gum-layout-engineSkill
- gum-layoutSkill
- gum-localizationSkill
- gum-monogame-renderingSkill
- gum-monthly-releaseSkill
- gum-project-versioningSkill
- gum-property-assignmentSkill
- gum-raylib-renderingSkill
- gum-releaseSkill
- gum-runtime-animation-chainsSkill
- gum-runtime-bindingSkill
- gum-runtime-fontsSkill
- gum-runtime-hot-reloadSkill
- gum-runtime-syntax-versionSkill
- gum-runtime-systemmanagers-initSkill
- gum-runtime-topologySkill
- gum-samplesSkill
- gum-serviceSkill
- gum-source-linked-projectSkill
- gum-themingSkill
- gum-tool-animationsSkill
- gum-tool-codegenSkill
- gum-tool-copy-pasteSkill
- gum-tool-delete-logicSkill
- gum-tool-dialogsSkill
- gum-tool-errorsSkill
- gum-tool-file-watchSkill
- gum-tool-font-generationSkill
- gum-tool-import-from-gumxSkill
- gum-tool-outputSkill
- gum-tool-pluginsSkill
- gum-tool-save-classesSkill
- gum-tool-screenshotsSkill
- gum-tool-selectionSkill
- gum-tool-tree-viewSkill
- gum-tool-undoSkill
- gum-tool-variable-gridSkill
- gum-tool-variable-referencesSkill
- gum-tool-viewmodelsSkill
- gum-unit-testsSkill
- gum-variable-deep-diveSkill
- gum-visual-eventsSkill
- The first 60 shown.
The file
We split this skill into the rules it gives an agent
2 rules. About 0 are common, 1 is distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 18
No Roslyn dependency — lives in GumRuntime/GumCommon, available to all platforms.
- Very specific: line 59
After applying variable references, call GraphicalUiElement.RefreshStyles() or GumService.Default.RefreshStyles() to push the updated …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Runtime Variable References
Overview
Variable references defined in the Gum tool can be re-evaluated at runtime. The primary use case is theming: modify centralized style values in code, propagate them across all elements, then create UI.
Key API
ApplyAllVariableReferences (GumRuntime)
Extension method on GumProjectSave in ElementSaveExtensions.GumRuntime.cs. Iterates all elements (standards, components, screens) and applies variable references on every state including category states (e.g., ColorCategory). Uses topological sort so dependencies are applied first — if B references A, A is applied before B. Handles circular dependencies gracefully (appends them at the end).
No Roslyn dependency — lives in GumRuntime/GumCommon, available to all platforms.
GumExpressionService (Gum.Expressions NuGet)
Located in Runtimes/GumExpressions/. Provides Roslyn-based expression evaluation for arithmetic in variable references (Width + 10, Width * 2). Optional — without it, only simple dot-path lookups work (OtherInstance.Width).
GumExpressionService.Initialize() sets ElementSaveExtensions.CustomEvaluateExpression to a Roslyn-based evaluator. The evaluator is EvaluatedSyntax, which was extracted from the Gum tool into this project. Conditional (ternary), comparison (==, !=, <, >, <=, >=), and logical (&&, ||, !) operators all flow through this same path — they work at runtime when Gum.Expressions is wired.
Two Apply Overloads (ElementSaveExtensions)
ApplyVariableReferences(ElementSave, StateSave)— writes hard values into the StateSave. Use before creating UI.ApplyVariableReferences(GraphicalUiElement, StateSave)— sets properties on live runtime visuals viaSetProperty.
Values the saved type cannot hold (#6414)
A reference carries what code set, not only what a project can store. Reference reads use TryGetPropertyExact (internal; the public TryGetProperty stays save-safe for the snapshot serializer): X/Y units with no PositionUnitType value (PercentageOfFile, PercentageOfOtherDimension, MaintainFileAspectRatio, X PixelsFromMiddleInverted) read as the GeneralUnitType, and EvaluatedSyntax.CastTo leaves a GeneralUnitType alone instead of reinterpreting its number. SetProperty("FontSize", ...) accepts a float. Known gap: with Gum.Expressions a fractional FontSize reaches a reference as an int, because every result is cast to the saved variable type (FontSize is int; fixing it needs a saved-format change). TypedSetterSweep asserts exact values for every variable and enum value and names that gap (IsKnownExpressionGap).
Variables of different types without Gum.Expressions
Without Gum.Expressions nothing converts the right side, so a value of another declared type would throw in a setter (or blank a string). TryReadReference and ReportedMismatch (ElementSaveExtensions.GumRuntime.cs) compare the declared types of both variables for every row, at creation, live and in ApplyAllVariableReferences. A mismatch keeps the target's value and reports through CustomSetPropertyOnRenderable.RaisePropertyAssignmentError (the [Gum] Console.Error line plus PropertyAssignmentError), reached from GumRuntime by the internal hook ElementSaveExtensions.ReportPropertyAssignmentError that each platform wires; each owner reports a given row once (IsFirstReport), so a row that changes every frame does not flood the console; the text says to add Gum.Expressions only when it converts that pair (int/float/double among themselves, anything into a string, a string into an enum). What the setters already take is not a mismatch: any int or float into FontSize, an int into an enum. Tests: ReferenceBetweenMismatchedTypesTests. The check judges declared types, not values, because a live read can hand a float to an int variable (FontSize), which would pass a value check at update time and fail at creation. Live evaluation seeds untyped values, so FindSourceType looks the declared type up on the element and caches it; a valid row must allocate nothing extra (ReferenceReevaluationAllocationTests).
A cast Gum.Expressions refuses
With Gum.Expressions the package only adds evaluation power and never does worse than no package: a reference it cannot apply is reported like the same pair without it, never dropped silently, and a pair that applies without it applies with it. EvaluatedSyntax.CastTo returns false for a bool or text into a number, an enum into a number, a bool, float or double into an enum, a name that is no value of the enum, an enum of another enum type, and a float or double the int or float cannot hold. A type it does not know (a state category such as ButtonCategory, or a variable with no declared type) passes the value through unchanged, as without the package: the setter applies a text naming a state and ignores anything else, in both modes. A refused cast makes the service return null, so the evaluator leaves the refused value in the thread-static ElementSaveExtensions.RejectedCastValue (a side channel, not a second evaluation, so a refused live row evaluates once like any row); TryReadReference clears it before each evaluation, reads it after, and ReportedRejectedCast reports it once per row per owner through the shared ReportNotApplied wording, keeping the variable's value. A missing variable has no value and stays silent, with or without the package. So does a state name that does not exist: it is never reported, with or without the package, because Forms controls assign logical state names on purpose, implement only the states they choose, and the visual state for an unimplemented one is thrown away as intended. A NaN or infinity into a non-nullable float or double variable is refused the same way (IsNonFiniteForNumberVariable, reported through ReportedRejectedCast), because the X, Y, Width, Height, Rotation and FontScale setters throw on one under FULL_DIAGNOSTICS and take it silently otherwise; a nullable float (MaxWidth) still takes infinity. Test: ReferenceRejectedByTargetTests. ReportedMismatch judges only the no-package path. Tests: ReferenceFailedCastTests (the source x target matrix and the package-never-worse differential), ReferenceFailedCastAllocationTests.
Nullable variables
A typed set reports null too (label.MinWidth = null reaches Shadow.MinWidth = MinWidth). Live re-evaluation (VariableReferenceGraph.Evaluate) writes a null result only to a variable whose type ends in ? and only when the right side is a plain reference to an existing variable of a compatible type (MinWidth = Other.MinWidth; a bool or string read into a float? is a failed cast); any other null (unresolved name, failed expression, arithmetic on null) means "nothing evaluated" and keeps the value. EvaluatedSyntax.CastTo accepts float?/int? targets. TypedSetterSweep (shared by the MonoGame and Skia tests) covers nullable variables with and without Gum.Expressions, including set back to null.
Architecture
GumCommon (no Roslyn)
↑
Gum.Expressions (adds Roslyn) — optional NuGet
↑ ↑
Gum Tool Game (opt-in)The decoupling mechanism is ElementSaveExtensions.CustomEvaluateExpression — a static Func<StateSave, string, string, GraphicalUiElement?, object> delegate. When null, falls back to RecursiveVariableFinder (simple lookups only). When set by GumExpressionService.Initialize(), uses Roslyn for full expression support. The 4th argument is the live, already-laid-out GraphicalUiElement being applied against (when one exists) — it lets a reference resolve the runtime-computed Absolute* properties, which don't exist on authored StateSave data at all (see gum-tool-variable-references for the resolution mechanism). ApplyVariableReferences(GraphicalUiElement, StateSave) supplies its own top-level element automatically; ApplyVariableReferences(ElementSave, StateSave) only resolves Absolute* references when called with its optional liveRoot argument.
After applying variable references, call GraphicalUiElement.RefreshStyles() or
GumService.Default.RefreshStyles() to push the updated values to live visuals. For a deep
dive into how this works end-to-end, see the gum-variable-deep-dive skill.
global::Localization.CurrentLanguage
Reserved identifier (int, mirrors ILocalizationService.CurrentLanguage) resolved in EvaluatedSyntax against Gum.Localization.LocalizationRuntimeState.Current — a GumCommon-level static so Gum.Expressions can read it without depending on any platform runtime. CustomSetPropertyOnRenderable.LocalizationService (per-platform-compiled) forwards to it. VariableReferenceGraph.ScanReads lists it as the read VariableReferenceGraph.CurrentLanguageReadName, and GraphicalUiElement.RefreshLocalization (run by GumService on CurrentLanguageChanged) reports that name to each element with an ElementSave, so same-component rows reading the language re-evaluate on a switch with no explicit call. Cross-element rows still need ApplyAllVariableReferences() + RefreshStyles(). Tests: CurrentLanguageReferenceTests.
Live re-evaluation within a component
Rows on a component or screen that read a variable re-evaluate when it changes: see VariableReferenceGraph (GumCommon/Runtime) and GraphicalUiElement.NotifyVariablesChanged. SetProperty, ApplyState (batched, so animations need no call) and the typed X/Y/Width/Height/Rotation/Visible setters report changes. Any typed setter that calls NotifyPropertyChanged (such as TextRuntime.Text) reports through it, reading the new value with TryGetProperty; TextRuntime Red/Green/Blue/Alpha call the protected ReportTypedPropertyChanged(name) themselves. TextRuntime FontSize, IsBold, IsItalic and OutlineThickness call ReportTypedPropertyChanged too; TryGetProperty returns FontSize as the int the variable is, since the float would fail the (int) cast when written to another element. A typed property that is also a Gum variable reports on the runtimes the sweep covers (MonoGame, Skia, Apos), including the layout ones (StackSpacing, the *Units, *Origin, ChildrenLayout, Min/Max sizes and so on); TypedSetterSweep (Tests/Shared) lists what is covered and holds the skip list and its conditions, and VariableReferenceTypedSetterTests runs it and goes red for a new setter that does not report (a setter that wraps another, like DropshadowRed over DropshadowColor, must report its own name). A Skia shape runtime whose dispatcher branch in Runtimes/SkiaGum/CustomSetPropertyOnRenderable.cs writes a variable straight to the renderable (instead of calling TrySetPropertyOnRuntime first) leaves the runtime getter stale, so a reference never sees the value (#6406; the Svg SourceFile branch was the same, #6422). SvgRuntime/LottieAnimationRuntime SourceFile report the assigned path with ReportTypedPropertyChanged(name, string), since TryGetProperty("SourceFile") recovers a texture name, not the path; the sweep covers them (Sprite's SourceFile always reads null and is excluded, RenderTargetTextureSource is a renderable assigned by element name and is not swept). A property that changes other variables reports them too (#6410): GraphicalUiElement.NotifyPropertyChanged reports Red/Green/Blue/Alpha (and the 1/2 forms) for Color/Color1/Color2, ArcRuntime also reports Red1..Alpha1 for Red..Alpha/Color, and the sweep finds these by setting each property (including non-variable Color) on a fresh element and giving every variable that changed its own reference row. It runs for Apos shapes in MonoGameGum.Shapes.Tests; AposShapeRuntime setters call NotifyPropertyChanged. CurrentChainName is not swept (it keeps only a name the element has); CurrentChainNameReferenceTests covers it. Setting a nullable variable back to null is reported too (see Nullable variables). A property that calls neither NotifyPropertyChanged nor ReportTypedPropertyChanged needs RefreshReferences. Design: Direction/decisions/0022-reactive-variable-references-within-a-component.md.
- Live evaluation reads every input at its live value (#6411):
NotifyVariablesChangedseeds the evaluation state with the changed values plus the live value of each other input the affected rows read (AddLiveInputs). Typed values come fromTryGetProperty; a custom variable (no property on the visual) comes from_referenceInputs, the last value reported or written by a row (RecordReferenceInput). A null or unreadable input falls back to the authored value, since a null cannot be put in the state. Styling and cross-element references stay explicit. SetVariablesRecursivelymarks the element being loaded (BeginReferenceLoad): what it writes to that element, and rows reading that element from its container, report to nobody, since the load applies those rows itself. What it writes to the element's children reports as usual, so a screen override (Label1.Text), an exposed variable, a nested component's row or a hot reload reaches the child's own rows, which were evaluated at its creation against defaults. Do not wrap a load inBeginSuppressionagain: that silences the children too (#6385).VariableReferenceGraph.BeginSuppressionis for row evaluation only:Evaluatewrites each result silently, then notifies a child owner's own rows explicitly. A derived component whose base owns rows has them evaluated again after its own values apply (HasInheritedRows).- A new reporting path must check
GetReferenceAudiencebefore boxing a value; the zero-allocation guard isReferenceReevaluationAllocationTests.
@, % and Index at runtime
All three work in a game that calls GumExpressionService.Initialize(). The GraphicalUiElement overload of ApplyVariableReferencesOnSpecificOwner rewrites @ through ResolveOwnerPrefix, and Index reads the element's own instance order when no live tree is passed. Without Gum.Expressions, @Width still resolves as a plain lookup, but %, Index and every other operator need it (see gum-tool-variable-references).
Circular references (#6871)
Rows that read each other (or their own result) never freeze or overflow on any path (ReferenceCycleFreezeTests, ReferenceCycleToolFreezeTests). One rule for the tool, creation and ApplyAllVariableReferences: cyclic rows (VariableReferenceGraph.CyclicRows) are skipped everywhere and keep their stored values, so applying again is idempotent. The tool reports the loop as GUM0013 (HeadlessErrorChecker). The runtime reports it under FULL_DIAGNOSTICS only, once per loop: each separate loop in each state gets its own message, deduped per ElementSave by state and rows (ReportCyclicRows, from both apply overloads, through the shared RaiseReferenceError channel; a reloaded project has new elements and reports again). The message names the element and the state holding the loop (in Panel (Default):, like GUM0013). A loop made only of one base's rows (ReferenceRow.DeclaredIn) is reported on that base, once, not again on each derived element; a loop mixing base and derived rows is reported on the derived element, and stays silent and allocation-free otherwise. Test: ReferenceCycleDiagnosticTests.
One analysis of which rows read what
VariableReferenceGraph.ScanReads is the only place that decides what a row reads. ReferenceDependencies builds on it for cross-element questions, and ApplyAllVariableReferences, ObjectFinder.GetElementReferencesToThis and the tool's VariableReferenceLogic.ApplyReferencesToElement all call it. Match references through it, not by string prefix. ApplyVariableReferences(ElementSave, StateSave) runs a state's rows in VariableReferenceGraph.LoadOrderRows order, so written order does not matter.
A fresh load applies references like a reload (#6881)
A service that loads a project from files (GumService.FinishInitialize, GumServiceSkiaBase) calls ApplyAllVariableReferencesBeforeFirstBuild(); the project's rows are applied once, in ElementSave.ToGraphicalUiElement's outermost build, to whichever project is then in ObjectFinder. It waits for the first build because games call GumExpressionService.Initialize() after GumService.Initialize(). A project built in code or by the tool is never flagged, so a build does not change it. Tests: FreshLoadReferenceChainTests, GumServiceFreshLoadReferenceChainTests.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

