gum-layout-engine
$
npx mdskill add vchelaru/Gum/gum-layout-engineDebug Gum layout engine internals and optimize rendering performance.
- Analyzes UpdateLayout chains and dirty state tracking for performance issues.
- Integrates with GraphicalUiElement.cs to trace rendering logic.
- Decides execution by matching specific layout function keywords.
- Delivers actionable insights on stacking pipelines and dimension updates.
SKILL.md
.github/skills/gum-layout-engineView on GitHub ↗
---
name: gum-layout-engine
description: >
Deep reference for maintainers of the Gum layout engine — the UpdateLayout
call chain, UpdateChildren ordering, stacking position pipeline, dirty state
tracking, and performance optimizations inside GraphicalUiElement.
Load when debugging layout performance, optimizing UpdateLayout/UpdateChildren,
working on RefreshParentRowColumnDimensionForThis, GetWhatToStackAfter,
MakeDirty, ResumeLayoutUpdateIfDirtyRecursive, or _cachedSiblingIndex.
trigger_phrase: UpdateLayout internals|UpdateChildren|GetWhatToStackAfter|RefreshParentRowColumnDimensionForThis|MakeDirty|ResumeLayoutUpdateIfDirtyRecursive|_cachedSiblingIndex|layout performance|ChildrenUpdateDepth|GetIfShouldCallUpdateOnParent|UseFixedStackChildrenSize|UpdateLayoutCallCount
---
# Gum Layout Engine Internals
For user-facing layout concepts (units, stacking, wrapping, Anchor/Dock), see
the **gum-layout** skill. This skill is for people debugging, optimizing, or
extending the engine itself.
All layout logic lives in `GumRuntime/GraphicalUiElement.cs`.
## UpdateLayout Call Chain
Entry point: `UpdateLayout(ParentUpdateType, int childrenUpdateDepth, XOrY?)`
### Flow (in order)
1. **Resolve `updateParent`** — evaluate `ParentUpdateType` flags against actual
parent state (stacks? depends on children? has ratio children?).
2. **Early out — suspended or invisible** — if layout is suspended
(`mIsLayoutSuspended` or `IsAllLayoutSuspended`) OR the element is invisible
and not needed for parent update, call `MakeDirty()` and return. Invisible
elements also exit if parent is invisible (unless render target).
3. **Early out — propagate to parent** — if `updateParent` is true AND
`GetIfShouldCallUpdateOnParent()` is true, call `parent.UpdateLayout()` with
`childrenUpdateDepth + 1` and **return**. The parent's layout will update
this element as a child. This is how child changes bubble up.
4. **Clear dirty state** — `currentDirtyState = null`. This is critical: it
prevents double-updates during `ResumeLayoutUpdateIfDirtyRecursive` (see
below).
5. **Pre-children dimensions** — update dimensions that do NOT depend on
children (Absolute, PercentageOfParent, etc.) so children have correct parent
sizes when they lay out.
6. **First children pass (if dimensions depend on children)** — update children
with absolute layout types so the parent can measure them. With
`UseFixedStackChildrenSize`, only the first child is updated here (O(1)
instead of O(n)).
7. **Post-children dimensions** — update RelativeToChildren / RelativeToMaxParentOrChildren
dimensions now that children have been measured.
8. **Wrapped children pass** — if `WrapsChildren`, update `StackedWrapped`
children and re-measure dimensions with wrapping considered.
9. **UpdatePosition** — calculate X/Y based on units, origin, parent stacking.
For stacked children, this calls `GetWhatToStackAfter` to find position
relative to the previous sibling.
10. **RefreshParentRowColumnDimensionForThis** — if parent stacks, update the
per-row/column max dimension.
11. **Full children pass** — `UpdateChildren(depth, ChildType.All, ...)` updates
all children. Children already updated in step 6 are skipped via
`alreadyUpdated` set.
12. **Post-layout dimension check** — if size changed and parent depends on
children, re-update dimensions. If still changed, update parent.
## UpdateChildren Internals
### Two-pass ordering for Ratio dependencies
When some children use `Ratio` width/height and siblings use complex units
(RelativeToChildren, PercentageOfOtherDimension, MaintainFileAspectRatio,
ScreenPixel, RelativeToMaxParentOrChildren), those complex-unit siblings must be
updated **first**. Ratio children need sibling sizes to compute remaining space.
Pass 1 (conditional): update children with `DoesDimensionNeedUpdateFirstForRatio`
units. Pass 2: update all remaining children.
### shouldFlagAsUpdated
For `Regular` layout, children are flagged as updated to avoid redundant work.
For stacked layouts, children are **not** flagged — they need a second pass to
update positions in order (stacking depends on sibling order).
### _cachedSiblingIndex
Set on each child (`child._cachedSiblingIndex = i`) in the iteration loop
before calling `UpdateLayout`. Used by `GetWhatToStackAfter` to avoid an
O(n) `IndexOf` call. Falls back to `IndexOf` if the cache is stale (element
not at expected position).
## Stacking Position Pipeline
`UpdatePosition` → `TryAdjustOffsetsByParentLayoutType` → `GetWhatToStackAfter`
### GetWhatToStackAfter
Finds the previous visible sibling and computes the offset to stack after it.
1. **Find sibling index** — uses `_cachedSiblingIndex` (O(1)) with `IndexOf`
fallback (O(n)). The cache is valid during `UpdateChildren` but may be stale
for individual property-change-triggered layouts.
2. **Find previous visible sibling** — walks backward from `thisIndex` skipping
invisible elements.
3. **Determine wrap** — if wrapping, increments `StackedRowOrColumnIndex` and
sums `StackedRowOrColumnDimensions` for all previous rows/columns.
4. **Compute offset** — for non-wrapping: previous sibling's position + size +
`StackSpacing`. For wrapping: row/column dimension sum.
### RefreshParentRowColumnDimensionForThis
Maintains `parent.StackedRowOrColumnDimensions[rowOrColumnIndex]` — the max
cross-axis dimension for each row (LeftToRight) or column (TopToBottom).
**O(1) fast path**: if this child's dimension >= stored max, just set it.
This is the common case during sequential layout (e.g., populating a ListBox).
**O(n) fallback**: if this child's dimension < stored max, it may have been the
max-holder and shrunk. Must rescan all siblings in the same row/column to find
the true max.
## Dirty State and Suspension
### MakeDirty
Called when `UpdateLayout` is invoked on a suspended or invisible element.
Accumulates into `currentDirtyState`:
- `ParentUpdateType` — OR'd together across multiple calls
- `ChildrenUpdateDepth` — max of all calls
- `XOrY` — set to null if different axes were dirtied (means update both)
### ResumeLayoutUpdateIfDirtyRecursive
Called when layout is resumed after suspension. Walks the tree:
1. Clear `mIsLayoutSuspended`
2. If `currentDirtyState != null`, call `UpdateLayout` with accumulated state
3. Recurse into children
**No double-update**: the parent's `UpdateLayout` (step 2) calls
`UpdateChildren`, which calls each child's `UpdateLayout`, which clears that
child's `currentDirtyState`. When recursion (step 3) reaches that child, its
dirty state is already null — it skips the `UpdateLayout` call.
### EffectiveDirtyStateParentUpdateType
Combines `currentDirtyState.ParentUpdateType` with runtime checks:
`GetIfParentHasRatioChildren()` and `GetIfParentStacks()`. These are checked
at resume time, not when dirtied, so they reflect current state.
## Upward Propagation
### GetIfShouldCallUpdateOnParent
Returns true if:
- Parent dimensions depend on children (`GetIfDimensionsDependOnChildren`)
- Parent stacks children (any non-Regular `ChildrenLayout`)
- Any sibling uses `Ratio` width/height
When true, `UpdateLayout` delegates to the parent (step 3 above) instead of
laying out the element directly. The parent will re-lay out all children.
## Performance Patterns
| Optimization | What it avoids | Where |
|---|---|---|
| `_cachedSiblingIndex` | O(n) `IndexOf` per child → O(n²) total | `GetWhatToStackAfter` |
| `RefreshParentRowColumn` fast path | O(n) rescan per child → O(n²) total | `RefreshParentRowColumnDimensionForThis` |
| `UseFixedStackChildrenSize` | Iterating all children in `GetMaxCellHeight` | `UpdateLayout` step 6, `UpdateHeight` |
| `xOrY` parameter | Recalculating unchanged axis | Throughout |
| `childrenUpdateDepth` | Unbounded recursion | `UpdateChildren` decrements per level |
| `alreadyUpdated` set | Re-updating children measured in pre-pass | `UpdateChildren` |
### Diagnostic Counters
- `UpdateLayoutCallCount` — total layout calls (incremented after parent propagation check)
- `ChildrenUpdatingParentLayoutCalls` — times a child triggered parent relayout
More from vchelaru/Gum
- bump-nuget-versionBump the NuGet package versions for all 12 Gum projects (11 libraries + GumCli). Queries NuGet to check if a version exists for today, then sets the new version to YYYY.M.D.V where V increments from the latest published version today (or starts at 1). Creates a release branch named ReleaseCode_YYYY_M_D_V, commits the changes, and pushes. Run this before triggering the nuget release workflow.
- gum-cliReference guide for GumCli — the headless command-line tool for Gum projects. Load this when working on gumcli commands (new, check, codegen, codegen-init), Gum.ProjectServices, HeadlessErrorChecker, ProjectLoader, HeadlessCodeGenerationService, CodeGenerationAutoSetupService, or the FormsTemplateCreator.
- gum-docs-writingReference guide for writing Gum documentation in GitBook markdown. Load when writing or editing docs/ files, adding pages to SUMMARY.md, using GitBook hints/figures, linking between pages, or adding images.
- gum-forms-behaviorsCovers Gum's behaviors system and the design-time → runtime Forms wrapping lifecycle. Load this when working on BehaviorSave, ElementBehaviorReference, StandardFormsBehaviorNames, FormsUtilities.RegisterFromFileFormRuntimeDefaults, DefaultFromFileXxxRuntime classes, or when investigating why Forms properties cannot be set at design time in the Gum tool.
- gum-forms-controlsReference guide for Forms controls — classes inheriting from FrameworkElement. Load this when working on Button, CheckBox, ListBox, ComboBox, TextBox, ScrollViewer, or any class in Gum.Forms.Controls (or FlatRedBall.Forms.Controls). Also load when working on FrameworkElement itself, the Visual/InteractiveGue relationship, state machines, DefaultVisuals, or ReactToVisualChanged.
- gum-forms-default-visualsReference guide for Forms DefaultVisuals — the code-only visual classes that back Forms controls. Load when working on ButtonVisual, any *Visual class in DefaultVisuals/, Styling, DefaultFormsTemplates registration, or building custom code-only Forms visuals.
- gum-forms-itemscontrolReference guide for ItemsControl and ListBox — the Items/ListBoxItems relationship, templates, InnerPanel sync, and gotchas. Load this when working on ItemsControl, ListBox, ListBoxItem, VisualTemplate, FrameworkElementTemplate, Items collection behavior, ListBoxItems desync, or adding/removing items from a list box.
- gum-layout>
- gum-localizationReference guide for Gum's runtime localization system — ILocalizationService, CSV/RESX loading, Text vs TextNoTranslate paths, Forms control localization patterns, and gotchas.
- gum-property-assignmentReference guide for how Gum applies variables and sets properties on renderables. Load this when working on ApplyState, SetProperty, SetVariablesRecursively, CustomSetPropertyOnRenderable, font loading, IsAllLayoutSuspended, or isFontDirty.