Gum Property Assignment Reference
SkillDev toolsHow Gum instantiates runtime objects from save data and applies variables to renderables. Triggers: ToGraphicalUiElement, SetGraphicalUiElement, ApplyState, SetProperty, SetVariablesRecursively, CustomSetPropertyOnRenderable, font loading, IsAllLayoutSuspended, isFontDirty.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Gum Property Assignment Reference skill
What this skill tells your AI
The instructions your AI receives, as published by vchelaru/gum in .claude/skills/gum-property-assignment/SKILL.md and read by ahel’s review.
Save-to-Runtime Instantiation
When a game loads a Gum project, ElementSave.ToGraphicalUiElement() is the entry point that
creates a live GraphicalUiElement tree from a save element (screen, component, or standard).
Without a loaded Gum project, ElementSave classes are not used at runtime — but StateSave,
StateSaveCategory, and VariableSave are always used (they power the state system on GUE).
ToGraphicalUiElement → SetGraphicalUiElement pipeline
ToGraphicalUiElement (in ElementSaveExtensions.GumRuntime.cs) creates a GUE and delegates
to SetGraphicalUiElement, which does the heavy lifting in this order:
- AddStatesAndCategoriesRecursivelyToGue — walks the inheritance chain (via
BaseType) and copiesStateSaveCategoryandStateSaveentries onto the GUE. - CreateGraphicalComponent — creates the underlying renderable (
IRenderable) viaCustomCreateGraphicalComponentFuncand assigns it withSetContainedObject. - AddExposedVariablesRecursively — registers exposed variable bindings.
- CreateChildrenRecursively — iterates
elementSave.Instances, callsinstance.ToGraphicalUiElement()for each, and parents the child GUE. For screens, children are not parented directly (they useElementGueContainingThisinstead). - SetInitialState — applies the default state's variables via
ApplyState. - Animations — if the project has
ElementAnimations, wires them up.
After this, the GUE tree is fully created and has its default state applied. The GUE's Tag
and ElementSave properties point back to the source ElementSave.
For a deep dive into the full variable lifecycle including Forms state updates and
RefreshStyles, see the gum-variable-deep-dive skill.
Two Paths for Setting Properties on a GUE
Direct property setters (e.g. textRuntime.Font = "Arial") — these are typed C# properties
on TextRuntime or GraphicalUiElement that immediately call helpers like UpdateToFontValues().
String-based path (e.g. SetProperty("Font", "Arial")) — used by ApplyState and
SetVariablesRecursively when processing state variables. This path goes through:
ApplyState / SetVariablesRecursively
→ GraphicalUiElement.SetProperty(string, object)
→ CustomSetPropertyOnRenderable.SetPropertyOnRenderable(...)
→ TrySetPropertyOnText / TrySetPropertyOnSprite / etc.
→ modifies the underlying renderable directly
CustomSetPropertyOnRenderable is the bridge between the string-based variable system and the
actual renderable objects. It lives in Gum/Wireframe/CustomSetPropertyOnRenderable.cs (with
parallel copies in Runtimes/SkiaGum/ and Runtimes/RaylibGum/).
The delegate GraphicalUiElement.UpdateFontFromProperties is wired to the static
CustomSetPropertyOnRenderable.UpdateToFontValues(IText, GUE) at startup. This is how the
string path and the instance method path both ultimately call the same font loading logic.
All CustomSetPropertyOnRenderable statics (including FontService) are wired by
EditorTabPlugin_XNA.StartUp() in the Gum tool. The class itself must not reference DI
containers or service locators. Game runtimes assign FontService directly.
CustomSetPropertyOnRenderable's dispatch tree actually mixes renderable-type and Runtime-type
branches, and which one a property takes isn't obvious from this pipeline alone — see
gum-architecture-layers for why both exist and which one new branches should prefer.
Combining Variables
Several independent variables can combine into one derived value — X/XUnits into position,
Font+FontSize+IsBold+... into a loaded BitmapFont (below), or a boolean opt-out flag alongside
a text/state value into the final displayed string. SetProperty never peeks across variables to
sequence or dedupe this: each contributing setter independently recomputes the full derived value
from whatever is currently live on the object, so any application order converges to the same
result, at the cost of tolerated redundant recomputation (e.g. re-deriving a font twice if both
Font and FontSize land in the same batch).
Font Deferred-Loading System
Loading a .fnt file is expensive. A text element has ~6 font-related properties (Font,
FontSize, IsBold, IsItalic, UseFontSmoothing, OutlineThickness), so without deferral each
property assignment during a screen load triggers a separate disk read.
The Flag: isFontDirty
GraphicalUiElement.isFontDirty is set to true instead of loading the font when layout is
suspended. It is consumed (font loaded, flag cleared) by UpdateFontRecursive().
Where deferral happens
| Path | Defers for IsAllLayoutSuspended? | Defers for IsLayoutSuspended? |
|---|---|---|
GUE.UpdateToFontValues() (direct setters) | Yes | Yes |
CustomSetPropertyOnRenderable.UpdateToFontValues (string path) | Yes | No |
The string path deliberately does not defer for instance-level IsLayoutSuspended. Doing so
would cause cascading parent layout calls when UpdateFontRecursive later assigns the BitmapFont
to a Text with RelativeToChildren dimensions inside ResumeLayoutUpdateIfDirtyRecursive. See
the long comment at the top of that static method for the full explanation.
Where fonts actually load
Two code paths consume isFontDirty:
-
WireframeObjectManager(Gum tool screen load): afterIsAllLayoutSuspended = false, callsRootGue.UpdateFontRecursive()thenRootGue.UpdateLayout(). At this point all elements havemIsLayoutSuspended = false(becauseApplyStateskipsSuspendLayoutwhen the global flag is set), so every dirty text element loads its font in one pass. -
ResumeLayoutUpdateIfDirtyRecursive(instance-level suspension): setsmIsLayoutSuspended = falseon the current element before callingUpdateFontRecursive(), then recurses to children. This ordering is critical — ifmIsLayoutSuspendedwere still true whenUpdateFontRecursiveruns, that element's font load would be skipped. -
UpdateLayoutitself (#2999): each node realizes its own deferred font at the top of its layout body, before it measures itself. So a bareUpdateLayout()after liftingIsAllLayoutSuspendedloads deferred fonts too — callers don't have to also callUpdateFontRecursive. The font assignment's own follow-upUpdateLayout(theRelativeToChildrenbranch in the string-pathUpdateToFontValues) is suppressed during this flush viaGraphicalUiElement.SuppressLayoutFromFontChange, since the in-progress pass already sizes the element;isFontDirtyis cleared before the load so the suppressed re-layout can't re-trigger it.
Known gap
When fonts are set via the string path (SetProperty) during an ApplyState that uses
instance-level SuspendLayout (not IsAllLayoutSuspended), fonts still load immediately —
one disk read per property assignment. This is the MonoGame ApplyState path; fixing it
requires solving the cascading layout problem described in CustomSetPropertyOnRenderable.
Why deferral is font-specific: 1:1 file references load immediately
Deferral exists only because ~6 font properties (Font, FontSize, IsBold, IsItalic,
UseFontSmoothing, OutlineThickness) collapse into a single cache filename — without
isFontDirty, each property assignment during a load would trigger a redundant intermediate disk
read of the same .fnt.
A 1:1 file-reference asset has no such many-to-one mapping: one property holds one path that maps
to one file. So textures (SourceFile → AssignSourceFileOnSprite) and render-target shaders
(SourceShaderFile → AssignSourceShaderFileOnContainer, #3206) resolve immediately on
assignment — no dirty flag, no deferral. Both cache the resolved asset in LoaderManager by
normalized path so a file referenced by multiple elements loads once.
Key Files
| File | Role |
|---|---|
GumRuntime/GraphicalUiElement.cs | SetProperty, ApplyState, UpdateToFontValues (instance), UpdateFontRecursive, isFontDirty |
Gum/Wireframe/CustomSetPropertyOnRenderable.cs | String-path dispatch + static UpdateToFontValues(IText, GUE) |
GumRuntime/ElementSaveExtensions.cs | SetVariablesRecursively — iterates state variables and calls ApplyState |
Gum/Wireframe/WireframeObjectManager.cs | Sets IsAllLayoutSuspended around screen load, calls UpdateFontRecursive after |
Tool/EditorTabPlugin_XNA/MainEditorTabPlugin.cs | Wires all CustomSetPropertyOnRenderable statics in StartUp() |
Signals
- GitHub stars
- 620
- Forks
- 80
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
gum-property-assignment- Source
- github.com/vchelaru/gum