# Gum Property Assignment

> Gum Property Assignment Reference

- Skill: `vchelaru/gum-property-assignment` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vchelaru/gum-property-assignment`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vchelaru/gum-property-assignment/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: vchelaru (https://skillmd.com/u/vchelaru)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/vchelaru/gum-property-assignment

---


# Gum Property Assignment Reference

## 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

| 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`:

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 (`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()` |

