name: gum-forms-controls description: Forms controls (FrameworkElement subclasses). Triggers: Button, CheckBox, ListBox, ComboBox, TextBox, ScrollViewer, anything in Gum.Forms.Controls or FlatRedBall.Forms.Controls, FrameworkElement itself, the Visual/InteractiveGue relationship, state machines, DefaultVisuals, ReactToVisualChanged.
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) onFrameworkElement - No direct
Background,Foreground, orBorderBrushproperties — 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 theInteractiveGueto itsFrameworkElementFrameworkElement.X/Y/Width/Height/etc.— all forward toVisual; there is no separate logical sizingActualWidth/ActualHeight— computed pixel values, read fromVisual.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:
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 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.