# Gum Forms Controls

> Gum Forms Controls Reference

- Skill: `vchelaru/gum-forms-controls-2` (Agent Skill)
- Install (CLI): `npx skillmds@latest add vchelaru/gum-forms-controls-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/vchelaru/gum-forms-controls-2/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-forms-controls-2

---


# Gum Forms Controls Reference

## What They Are

Forms controls are classes inheriting from `FrameworkElement` (`MonoGameGum/Forms/Controls/FrameworkElement.cs`). Their names and API intentionally mirror WPF (Button, CheckBox, ListBox, TextBox, etc.), but the visual and layout engine is Gum (`GraphicalUiElement`/`InteractiveGue`), not WPF.

**WPF conventions that do NOT apply here:**
- No `Margin`, `Padding`, `HorizontalAlignment`, `VerticalAlignment` (in the WPF sense) on `FrameworkElement`
- No direct `Background`, `Foreground`, or `BorderBrush` properties — visual styling is done through states
- No WPF grid/dock/wrap panel auto-layout — sizing and positioning follow Gum's unit system (`X`, `Y`, `XUnits`, `YUnits`, `Width`, `Height`, `WidthUnits`, `HeightUnits`)

## The Visual / FrameworkElement Split

Every `FrameworkElement` has a `Visual` property of type `InteractiveGue` (which is `GraphicalUiElement`). The Visual owns all layout and rendering; the `FrameworkElement` is a logical/behavioral layer on top.

- `Visual.FormsControlAsObject` — back-link from the `InteractiveGue` to its `FrameworkElement`
- `FrameworkElement.X/Y/Width/Height/etc.` — all forward to `Visual`; there is no separate logical sizing
- `ActualWidth` / `ActualHeight` — computed pixel values, read from `Visual.GetAbsoluteWidth/Height()`

**The control does not own or know its visual's internal structure.** The `Visual` is arbitrary and pluggable — the code-only default (`*Visual`/`Default*Runtime`), a tool-authored component with Forms behaviors, or a custom `InteractiveGue` subclass — and the control knows only the **named children** it looks up in `ReactToVisualChanged` (which may be absent). So "a Window has an `InnerPanelInstance` that fills it" is a property of *one visual*, not of `Window`. Any feature that depends on internal layout (e.g. sizing the content host to children) must live in a **visual**, never in the control, and cannot be assumed across visuals — which is why `WindowVisual.MakeSizedToChildren()` is on the visual. See **gum-forms-default-visuals**.

## Two Construction Paths

**Forms-first** (`new Button()`): The default constructor calls `GetGraphicalUiElementFor(this)` which looks up `DefaultFormsTemplates` (or the older `DefaultFormsComponents`) to find the registered visual type, instantiates it with `createFormsInternally: false`, and assigns it as `Visual`. This path requires the type to be registered before the constructor runs.

**Visual-first** (`new ButtonVisual()`): The `DefaultVisuals` classes (in `MonoGameGum/Forms/DefaultVisuals/`) are `InteractiveGue` subclasses that construct all child runtimes, set up the state machine, and then call `FormsControlAsObject = new Button(this)` in their constructor. The two-bool constructor `(bool fullInstantiation, bool tryCreateFormsObject)` controls this — `tryCreateFormsObject: false` skips creating the Forms object, used when the visual is being instantiated by a Forms-first control.

## ReactToVisualChanged

When `Visual` is assigned, `ReactToVisualChanged()` fires. Subclasses override this to grab references to named children:

```csharp
protected override void ReactToVisualChanged()
{
    textComponent = Visual.GetGraphicalUiElementByName("TextInstance");
    coreTextObject = textComponent?.RenderableComponent as IText;
    base.ReactToVisualChanged();
}
```

Named child lookup is the standard pattern — controls depend on specific child names being present in the visual. Properties like `Button.Text` silently no-op (or throw in `FULL_DIAGNOSTICS` mode) if the expected child is absent.

## Visual States (Not WPF Styles)

Appearance changes are driven by a `StateSaveCategory` on the Visual. `UpdateState()` is called whenever interaction state changes and applies the correct state by name:

```csharp
Visual.SetProperty("ButtonCategoryState", stateName);
```

Common state names are defined as constants on `FrameworkElement` (`EnabledStateName`, `DisabledStateName`, `HighlightedStateName`, `PushedStateName`, `FocusedStateName`, etc.). The `GetState(string stateName)` method searches all categories on the Visual.

To customize appearance, either replace the Visual with a custom one that has different state variable values, or get and modify states directly via `control.GetState(...)`. For a deep dive into how variable values flow from save data through runtime application and how `RefreshStyles` pushes style changes to live Forms controls, see the **gum-variable-deep-dive** skill.

## Class Hierarchy

```
FrameworkElement
├── ButtonBase (IInputReceiver)
│   ├── Button
│   ├── ToggleButton
│   ├── RadioButton
│   └── CheckBox
├── ItemsControl (→ ScrollViewer)
│   ├── ListBox
│   └── ComboBox
├── ScrollViewer
│   └── (also base of ItemsControl)
├── TextBoxBase (IInputReceiver)
│   ├── TextBox
│   └── PasswordBox
├── Panel
│   └── StackPanel
├── Label
├── Slider (RangeBase)
├── ScrollBar (RangeBase)
├── ListBoxItem
├── MenuItem
├── UserControl
└── Splitter
```

**User-codegen'd screens and components also inherit from `FrameworkElement`** when `OutputLibrary` is `MonoGameForms` (the default for new projects). The generated partial class derives from `FrameworkElement` (or a matching built-in control when the element has Forms behaviors), so `this` inside `CustomInitialize` is a `FrameworkElement` — events like `AfterRefreshStyles` and overrides of `SaveRuntimeProperties` / `ApplyRuntimeProperties` are available on screens and components, not just on built-in controls like Button or Label. Only StandardElements (`Container`, `Text`, `Sprite`, etc.) remain non-Forms. See the **gum-tool-codegen** skill for how codegen picks the base class.

## Key Files

| Path | Purpose |
|------|---------|
| `MonoGameGum/Forms/Controls/FrameworkElement.cs` | Base class: Visual link, layout forwarding, state constants, construction |
| `MonoGameGum/Forms/Controls/Primitives/ButtonBase.cs` | Push/click/hold input handling |
| `MonoGameGum/Forms/Controls/ItemsControl.cs` | Items collection, InnerPanel management, ListBoxItemsInternal sync |
| `MonoGameGum/Forms/DefaultVisuals/` | Pre-built `InteractiveGue` subclasses that create state machines and Forms objects |
| `MonoGameGum/Forms/Controls/FrameworkElementExt.cs` | Extension helpers (AddToRoot, etc.) |

## Non-Obvious Behaviors

**Layout is Gum layout, not WPF layout.** A `Button` with default `Width = 128` and `WidthUnits = Absolute` is 128 pixels wide regardless of content — there is no WPF-style `Auto` sizing unless `WidthUnits = RelativeToChildren`. Do not expect WPF layout rules.

**`IsVisible` vs `Visibility`**: There is no `Visibility` enum. `IsVisible` maps directly to `Visual.Visible` (bool).

**No color shortcut on the control.** To change a button's color, either modify the state's variables on the Visual, or access the child runtime directly (`(listBox.Visual as ButtonVisual)?.Background.Color = ...`). There is no `Background` property on `Button`.

**`ReactToVisualChanged` can fire before child references exist.** If `Visual` is reassigned at runtime, all `GetGraphicalUiElementByName` lookups re-run. Code that caches Visual children must refresh inside `ReactToVisualChanged`, not in the constructor.

**`ParentFrameworkElement` walks the Visual parent chain**, skipping non-Forms Gue nodes, until it finds a Gue whose `FormsControlAsObject` is a `FrameworkElement`. It does not just return the direct parent.

