Gum Property Assignment Reference

SkillDev tools

How 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.

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:

  1. AddStatesAndCategoriesRecursivelyToGue — walks the inheritance chain (via BaseType) and copies StateSaveCategory and StateSave entries onto the GUE.
  2. CreateGraphicalComponent — creates the underlying renderable (IRenderable) via CustomCreateGraphicalComponentFunc and assigns it with SetContainedObject.
  3. AddExposedVariablesRecursively — registers exposed variable bindings.
  4. CreateChildrenRecursively — iterates elementSave.Instances, calls instance.ToGraphicalUiElement() for each, and parents the child GUE. For screens, children are not parented directly (they use ElementGueContainingThis instead).
  5. SetInitialState — applies the default state's variables via ApplyState.
  6. 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

PathDefers for IsAllLayoutSuspended?Defers for IsLayoutSuspended?
GUE.UpdateToFontValues() (direct setters)YesYes
CustomSetPropertyOnRenderable.UpdateToFontValues (string path)YesNo

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:

  1. WireframeObjectManager (Gum tool screen load): after IsAllLayoutSuspended = false, calls RootGue.UpdateFontRecursive() then RootGue.UpdateLayout(). At this point all elements have mIsLayoutSuspended = false (because ApplyState skips SuspendLayout when the global flag is set), so every dirty text element loads its font in one pass.

  2. ResumeLayoutUpdateIfDirtyRecursive (instance-level suspension): sets mIsLayoutSuspended = false on the current element before calling UpdateFontRecursive(), then recurses to children. This ordering is critical — if mIsLayoutSuspended were still true when UpdateFontRecursive runs, that element's font load would be skipped.

  3. UpdateLayout itself (#2999): each node realizes its own deferred font at the top of its layout body, before it measures itself. So a bare UpdateLayout() after lifting IsAllLayoutSuspended loads deferred fonts too — callers don't have to also call UpdateFontRecursive. The font assignment's own follow-up UpdateLayout (the RelativeToChildren branch in the string-path UpdateToFontValues) is suppressed during this flush via GraphicalUiElement.SuppressLayoutFromFontChange, since the in-progress pass already sizes the element; isFontDirty is 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 (SourceFileAssignSourceFileOnSprite) and render-target shaders (SourceShaderFileAssignSourceShaderFileOnContainer, #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

FileRole
GumRuntime/GraphicalUiElement.csSetProperty, ApplyState, UpdateToFontValues (instance), UpdateFontRecursive, isFontDirty
Gum/Wireframe/CustomSetPropertyOnRenderable.csString-path dispatch + static UpdateToFontValues(IText, GUE)
GumRuntime/ElementSaveExtensions.csSetVariablesRecursively — iterates state variables and calls ApplyState
Gum/Wireframe/WireframeObjectManager.csSets IsAllLayoutSuspended around screen load, calls UpdateFontRecursive after
Tool/EditorTabPlugin_XNA/MainEditorTabPlugin.csWires 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