Gum gum-forms-controls
Reference 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.
git clone https://github.com/vchelaru/Gum
T=$(mktemp -d) && git clone --depth=1 https://github.com/vchelaru/Gum "$T" && mkdir -p ~/.claude/skills && cp -r "$T/.claude/skills/gum-forms-controls" ~/.claude/skills/vchelaru-gum-gum-forms-controls && rm -rf "$T"
.claude/skills/gum-forms-controls/SKILL.mdGum 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
(in the WPF sense) onVerticalAlignmentFrameworkElement - No direct
,Background
, orForeground
properties — visual styling is done through statesBorderBrush - 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.
— back-link from theVisual.FormsControlAsObject
to itsInteractiveGueFrameworkElement
— all forward toFrameworkElement.X/Y/Width/Height/etc.
; there is no separate logical sizingVisual
/ActualWidth
— computed pixel values, read fromActualHeightVisual.GetAbsoluteWidth/Height()
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:
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:
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
when FrameworkElement
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 |
|---|---|
| Base class: Visual link, layout forwarding, state constants, construction |
| Push/click/hold input handling |
| Items collection, InnerPanel management, ListBoxItemsInternal sync |
| Pre-built subclasses that create state machines and Forms objects |
| 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.
vs IsVisible
: There is no Visibility
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.
can fire before child references exist. If ReactToVisualChanged
Visual is reassigned at runtime, all GetGraphicalUiElementByName lookups re-run. Code that caches Visual children must refresh inside ReactToVisualChanged, not in the constructor.
walks the Visual parent chain, skipping non-Forms Gue nodes, until it finds a Gue whose ParentFrameworkElement
FormsControlAsObject is a FrameworkElement. It does not just return the direct parent.