gum-tool-codegen
Skill655 starsChanged 2 months agoMITView on GitHub
Gum tool C# code generation. Triggers: CodeGenerator, CodeOutputPlugin, generated code structure, .codsj settings, OutputLibrary selection, Forms codegen, state generation. For CLI/headless codegen see gum-cli.
In this repository68 other files it gives its agents.
- CLAUDE.mdCLAUDE.md
- debuggingSkill
- frb-build-verificationSkill
- frb-pixel-harnessSkill
- gum-architecture-layersSkill
- gum-bundleSkill
- gum-cliSkill
- gum-cross-platform-unificationSkill
- gum-docs-writingSkill
- gum-file-pathsSkill
- gum-forms-behaviorsSkill
- gum-forms-controlsSkill
- gum-forms-default-visualsSkill
- gum-forms-itemscontrolSkill
- gum-forms-theme-importSkill
- gum-iconsSkill
- gum-issue-creationSkill
- gum-kernsmith-integrationsSkill
- gum-layout-engineSkill
- gum-layoutSkill
- gum-localizationSkill
- gum-monogame-renderingSkill
- gum-monthly-releaseSkill
- gum-project-versioningSkill
- gum-property-assignmentSkill
- gum-raylib-renderingSkill
- gum-releaseSkill
- gum-runtime-animation-chainsSkill
- gum-runtime-bindingSkill
- gum-runtime-fontsSkill
- gum-runtime-hot-reloadSkill
- gum-runtime-syntax-versionSkill
- gum-runtime-systemmanagers-initSkill
- gum-runtime-topologySkill
- gum-runtime-variable-referencesSkill
- gum-samplesSkill
- gum-serviceSkill
- gum-source-linked-projectSkill
- gum-themingSkill
- gum-tool-animationsSkill
- gum-tool-copy-pasteSkill
- gum-tool-delete-logicSkill
- gum-tool-dialogsSkill
- gum-tool-errorsSkill
- gum-tool-file-watchSkill
- gum-tool-font-generationSkill
- gum-tool-import-from-gumxSkill
- gum-tool-outputSkill
- gum-tool-pluginsSkill
- gum-tool-save-classesSkill
- gum-tool-screenshotsSkill
- gum-tool-selectionSkill
- gum-tool-tree-viewSkill
- gum-tool-undoSkill
- gum-tool-variable-gridSkill
- gum-tool-variable-referencesSkill
- gum-tool-viewmodelsSkill
- gum-unit-testsSkill
- gum-variable-deep-diveSkill
- gum-visual-eventsSkill
- The first 60 shown.
The file
Files
Gum Tool Code Generation System
Full walkthrough in codegen-deep-dive.md: placement and namespace derivation, FullyInCode vs FindByName in detail, the custom-file contract, and the rename/delete/orphan reconciliation rules. Self-contained, so it also works handed to an agent on its own.
What It Is
The code generation system produces C# partial classes from Gum Screens and Components. Two files per element: .Generated.cs (auto-regenerated, never hand-edit) and .cs (user-editable stub with partial void CustomInitialize() hook). StandardElements are never generated.
Use OutputLibrary.GumForms on every runtime. The Code tab labels it "Gum Forms (recommended)". MonoGameForms is an [Obsolete] alias with the same persisted value (5); OutputLibrary.GetName() prints the current name, since Enum.ToString() may return either member of a shared value. Forms controls live in GumCommon and every runtime exposes the same Gum.GueDeriving and GumService names, so one generated shape serves MonoGame, KNI, FNA, raylib, Skia, Silk.NET, Stride and Unity. Only each runtime's input layer differs. The other OutputLibrary values are legacy, kept so existing projects keep generating. See gum-architecture-layers and gum-cross-platform-unification.
Architecture
Tool UI (Gum.csproj) Headless (Gum.Presentation / Gum.ProjectServices)
CodeOutputPlugin/ CodeOutputPlugin/Manager/ (Gum.Presentation)
MainCodeOutputPlugin CodeGenerationService, ParentSetLogic, RenameService
CodeWindow (Code tab) CodeGeneration/ (Gum.ProjectServices)
CodeGenerator (~5700 lines), CustomCodeGenerator
CodeGenerationFileLocationsService, CodeGenerationNameVerifier
VariableExclusionLogic, CodeOutputProjectSettingsManager
CodeOutputElementSettingsManagerCodeGenerationService (tool-side) generates one element with the tool's popups and output via CodeGenerator (shared engine); its GenerateCodeForAllElements hands the batch to HeadlessCodeGenerationService.GenerateCodeForElements, the same orchestrator gumcli codegen uses, so skipping NeverGenerate elements and writing the fallback file live in one place -- see the gum-cli skill.
Both files live on disk outside the .gumx, so a change to an element's identity has to reconcile them: RenameService covers rename, folder move and BaseType change, CodeFileDeleteService owns the delete decision, and OrphanCodeFileScanService is the catch-all for files that orphaned without passing through either. One invariant governs all three: .Generated.cs is derived data and can be removed freely, while the custom .cs is user-authored, unrecoverable through undo, and never goes without consent or outside the recycle bin.
A Code tab settings edit is the other trigger: CodeFileLocationWatcher sends one message when files must move (the migration in CodeFileMigrator) and another when only the namespace or base class they declare changes (CustomCodeHeaderUpdater, rewriting in place). CodeGenerationFileLocationsService.GetCustomCodeFileName is the one place the custom path is computed, so every caller (generation, rename, delete, migration planner, header updater) follows CustomCodeFolder; never derive it from the generated path with a string replace. The orphan scan finds a stray custom file beside, or in a previous custom folder of, a still-expected generated file through CustomCodeMapping. Both back up through CodeFileBackupService, so one Restore menu item undoes either.
Configuration (.codsj files)
Project-level: ProjectCodeSettings.codsj alongside the .gumx. Managed by CodeOutputProjectSettingsManager. Key settings: OutputLibrary, CodeProjectRoot, GeneratedCodeFolder, GeneratedCodeFolderPrefix, CustomCodeFolder (optional, relative to CodeProjectRoot; empty keeps the custom .cs beside the .Generated.cs), MarkGeneratedCodeFolder (opt-in, omitted while false; GeneratedFolderEditorConfig writes an .editorconfig with generated_code = true into the generated folder only when it is separate from the custom folder and the root, never edits a file lacking its header comment, and is synced by HeadlessCodeGenerationService after every batch and by CodeFileLocationWatcher on a settings edit), RootNamespace, ObjectInstantiationType, InheritanceLocation, AppendFolderToNamespace. Syntax and C# version detection read the csproj from CodeProjectCsprojLocator.FindCsproj: CsprojPath when set (no fallback if missing), else the csproj in CodeProjectRoot.
Settings page: CodeOutputSettingsMembers.BuildProjectSections builds the Code generation page as Output / Locations / Namespaces and usings sections (plus a collapsed Legacy one). Every row needs a one-line DetailText hint (label + hint are its search terms), a warning is IsDetailTextWarning, and the 190px label column is set per member (FirstGridLength), never on the shared DataUiGrid. Design language: Direction/settings-design/README.md.
Element-level: ElementName.codsj alongside the .gucx/.gusx. Managed by CodeOutputElementSettingsManager. Key settings: GenerationBehavior, namespace override, custom output path.
Key Enums
| Enum | Values | Notes |
OutputLibrary | XamarinForms(0), WPF(1), Skia(2), Maui(3), MonoGame(4), GumForms(5), Raylib(6), Silk(7) | GumForms ("Gum Forms") is the value for new projects on any runtime; the rest are legacy. Raylib and Silk support only ObjectInstantiationType.FindByName (see below) |
ObjectInstantiationType | FullyInCode, FindByName | FullyInCode generates all creation; FindByName wires references to externally-created instances |
InheritanceLocation | InGeneratedCode, InCustomCode | Controls which partial class file declares the base class |
VisualApi | Gum, XamarinForms | Internal enum; Gum for MonoGame/GumForms/Skia/raylib, XamarinForms for Xamarin/MAUI |
GenerationBehavior | NeverGenerate, GenerateManually, GenerateAutomaticallyOnPropertyChange | Per-element setting |
Generated Code Structure (in order)
- Using statements (auto-detected from instances)
- Namespace (root + optional folder path)
- Partial class with optional inheritance
- State enums (one per category)
- State properties with
ApplyState()calls - Instance fields
- Custom variables (user-defined properties)
- Exposed variables (delegate to child instances)
- Constructor chain:
InitializeInstances()thenAddToParents()thenApplyDefaultVariables() ApplyState()methodsApplyLocalization()(if enabled)partial void CustomInitialize()
Non-Obvious Behavior
GumForms .Visual wrapping -- When OutputLibrary is GumForms, property access on instances goes through .Visual (e.g., this.Visual for root, this.InstanceName.Visual for children). The generated code treats everything that is not a StandardElement as a Forms object.
Label .Visual is typed InteractiveGue -- A component rooted in the Text standard (Label) has Text-only members (Font, FontSize, IsBold, ...) on TextRuntime, not on Visual. Both the element's own code (BaseStandardRuntimeTypeName) and instances of it (InstanceStandardRuntimeTypeName, plus the exposed-variable getter/setter) cast Visual to TextRuntime; PrefixCastsVisualToTextRuntime stops GetVisualCast from casting twice. Don't extend a per-variable whitelist for this.
Forms base type from behaviors -- GumForms determines the generated base class by scanning the element's behaviors (e.g., ButtonBehavior maps to Button). The method GetGumFormsTypeFromBehaviors drives this.
Unsupported Forms base type -- A component on a non-Container standard (Sprite, NineSlice, ...) with no Forms behavior has no valid Forms base class. gumcli codegen reports it as an element error through FormsCodegenBaseTypeErrorSource and skips the element; GetInheritance still writes the same message into the class for callers that skip the error pass (the tool). gumcli check does not run it, since it depends on the output library.
Unsupported Forms base type -- A component on a non-Container standard with no Forms behavior has no valid Forms base class. CodeGenerator.GetUnsupportedFormsBaseTypeError is the one check: gumcli codegen reports it via FormsCodegenBaseTypeErrorSource and skips the element, and the tool's CodeGenerationService and Code tab display call it directly, since the tool's Code tab runs no error checker.
Screen inheritance resolution order -- CodeGenerator.GetInheritance for ScreenSave resolves inheritance in this priority: element.BaseType > projectSettings.DefaultScreenBase > library-appropriate fallback (FrameworkElement for GumForms, GraphicalUiElement otherwise). DefaultScreenBase defaults to empty string so users can switch OutputLibrary without stale base classes bleeding through.
Authored behavior FormsProperty values -- CodeGenerator.FillWithFormsPropertyAssignments writes values authored for BehaviorSave.FormsProperties (ToolTip, LargeChange, ...) as this.<Instance>.<Name> = ...; on the control (not through .Visual), per instance and for the element's own default state (at the end of ApplyDefaultVariables). The property is looked up by reflection on the real Forms control in GumCommon (not the FormsControlPlaceholders types), and its type picks the literal, so a name the control lacks or a value that does not convert is skipped like the runtime BehaviorFormsPropertyApplier. GetIfFormsPropertyIsAssignedOnControl is the one test both this pass and the regular variable pass use, so a name is never written twice or through .Visual (a component authoring the name in its own default state makes it a component variable without making it a Visual one). Declared defaults are never written; they equal the control's code default. A value authored in a category state is written the same way inside that state's case (after the instance's visual variables): at runtime ApplyState hands a name the visual lacks to the Forms control by reflection (TrySetCustomVariableOnFormsControl), so states do apply Forms properties.
Default Forms template registration -- Generated code, not the runtime loader, makes a component the default visual for its Forms type. CodeGenerator.GetDefaultFormsTemplateType emits DefaultFormsTemplates[typeof(<FormsType>)] = template (DefaultFormsComponents below syntax version 1) only when the element is its behavior's DefaultImplementation and the behavior is not in BehaviorsWithoutDefaultFormsTemplate. That registration overrides the V3 default visual. A shipped behavior missing from BehaviorGumFormsTypes generates as a plain FrameworkElement; adding it changes the base class of every component using that behavior.
State generation suppressed for Forms standards -- When OutputLibrary is GumForms and the state container is a StandardElementSave, state code is not generated; the Forms framework handles it.
Forms placeholders -- Gum.ProjectServices can't reference MonoGameGum, so FormsControlPlaceholders.cs hand-mirrors the real Forms controls' members. Codegen reflects on it to skip or new-mark generated members that would hide an inherited one (CS0108/CS0114). When a template component gains an exposed variable or state category that collides with a real base member, mirror that member (and its base class) there; FormsTemplateCodegenHidingTests in Gum.Cli.Tests compares generated output to the real types and fails on a miss. FormsControlPlaceholderTests fails when a behavior-mapped Forms type has no placeholder (or lacks a commonly exposed Text/Header/Placeholder), so a new entry in BehaviorGumFormsTypes needs its placeholder, with the real base class (CheckBox/RadioButton derive from the ToggleButton placeholder).
Legacy usings are translated at codegen time -- CodeGenerator.TranslateLegacyUsingStatements rewrites MonoGameGum.GueDeriving/SkiaGum.GueDeriving to Gum.GueDeriving at resolved syntax version >= 1 and MonoGameGum to Gum at >= 3 (the thresholds GetGueDerivingNamespace/GetGumServiceNamespace use), in both the generated file and the custom .cs stub. It filters the output only; the saved .codsj text is never rewritten, and older versions keep their usings. Keep the thresholds in step with those two methods.
Checked-in generated output -- Any change to generated-code output also requires regenerating the Tests/CodeGen_* projects (Tests/GenerateAllCodeGenProjects.bat: build Gum.Cli, run gumcli codegen on each) and committing the diff, or CI's "Codegen Drift Check" fails. Every generated .cs is written CRLF on every OS (GeneratedCodeText.ToCrLf, applied by CodeGenerator.GetGeneratedFileText and the ICodeFileWriter implementations), and git add normalizes it, so judge the diff with git diff --cached --stat, not git status. A new code-file writer must go through ToCrLf too. CI also compiles every bundled theme's Gum Forms output on fresh gumcli new projects through Tools/build-theme-projects.ps1 -Platform, once for monogame and once for kniskia, which has no output library of its own.
Missing dependency auto-generation -- When generating for an element, the system checks if referenced elements lack code files and offers to generate them too. In auto-generation mode this happens silently.
ObjectFinder cache -- Code generation enables/disables ObjectFinder.Self cache around generation loops for performance. Must be managed at the call site (not inside CodeGenerator).
VariableExclusionLogic -- Certain variables are excluded depending on OutputLibrary (e.g., Alpha excluded for XamarinForms). The plugin hooks into the VariableExcluded query event to apply this.
Engine profile variables are not generated (#7724) -- A variable the project's engine profile hides is dormant, like one that does nothing: kept in the file, not shown, not generated, no warning. CodeGenerator takes an optional IVariableRestrictions (the tool passes IProjectRestrictions; HeadlessCodegenRunner loads the profile with EngineProfileLoader and passes EngineProfileVariableRestrictions, warning on stderr when the file is unusable). The check is GetIfHiddenByEngineProfile, applied in the instance/state pass, the element's own variables, and Parent (a hidden Parent attaches the instance as if it had none). It uses the Variables tab's rule through VariableRestrictionsExtensions.IsVariableHidden: custom and state variables and Name/BaseType/Locked/DefaultChildContainer/State are exempt. The profile types live in Tools/Gum.ProjectServices/EngineProfiles. A type outside a non-empty SupportedStandardTypes is blocked, and so is anything derived from it (a component whose root standard type is blocked, and instances of it): IVariableRestrictions.IsStandardTypeBlocked plus IsElementBlocked/IsInstanceBlocked. Blocked instances are skipped at every per-instance emission site (and the variables, exposed variables and state variables sourced from them; a child's Parent pointing at one attaches the child to this), and a blocked component gets no generated or custom file (HeadlessCodeGenerationService, CodeGenerationService). DisableComponents/DisableBehaviors are not read by codegen.
Tool plugin auto-regeneration -- MainCodeOutputPlugin listens to nearly every edit event (variable set, instance add/delete, state changes, etc.) and auto-regenerates if the element's GenerationBehavior is GenerateAutomaticallyOnPropertyChange.
RequestCodeGenerationMessage -- External systems (like FlatRedBall editor integration) can trigger codegen via this CommunityToolkit.Mvvm message.
Culture-independent output -- Generated code must be identical on every machine, so sorting, casing and number formatting in the generators use the invariant culture (CultureInfo.InvariantCulture, StringComparison.Ordinal/InvariantCulture, ToUpperInvariant).
C# name compliance -- CodeGenerationNameVerifier prefixes C# keywords with @, leading digits with _, and replaces spaces with _. Text that is not a validated Gum name (the csproj file name behind RootNamespace, element folder segments in a namespace) goes through NameRules.ToCSharpIdentifier instead, which also replaces dots, hyphens and other invalid characters; never derive a namespace or identifier with ad-hoc Replace calls.
Member names that collide -- A member can't be named like its class (CS0542) or like a method the generator declares (CustomInitialize, ApplyDefaultVariables, ...). Every site that writes an instance, custom/exposed variable or animation member name uses CodeGenerationNameVerifier.ToCSharpMemberName, which appends _ to a reserved name; CodeGenerator.ReserveMemberNames sets the reserved names per element. The tool also refuses such instance names (NameVerifier.IsNameAlreadyUsed), so the rename only shows up in projects edited by hand. A new generated method name goes in GeneratedMethodNames. The category enum names are reserved too (an instance named like a category gets the _). A category's own names are public API, so they are not renamed: CodeGenerator.GetCategoryNameCollisionError reports a category whose enum or <Category>State property equals the generated class's name (library-dependent: legacy runtime classes end in Runtime) or another category's names. gumcli codegen blocks the element through CategoryNameCollisionErrorSource and the Code tab shows the message; gumcli check cannot, since the class name needs the output library.
RenameService -- When elements are renamed in the tool, updates generated code file names and internal references.
Legacy Raylib and Silk output -- OutputLibrary.Raylib and Silk emit the same shape as legacy MonoGame (no Forms wrapping), and support only ObjectInstantiationType.FindByName. CodeGenerator.UsesUnifiedGumRuntime is the shared predicate for these three values. AssertSupportedCombination throws NotSupportedException for FullyInCode, so the CLI exits 1; the tool calls CoerceToSupportedCombination to snap back to FindByName. ResolveSyntaxVersion floors both at syntax version 3 because they never had the pre-unification namespaces. GetGumServiceNamespace takes isRaylib because the legacy shim namespace is RaylibGum, not MonoGameGum; passing the wrong one emits an unresolvable using MonoGameGum;.
Key Files
| File | Purpose |
Tools/Gum.ProjectServices/CodeGeneration/CodeGenerator.cs | Core codegen engine (~5700 lines) |
Tools/Gum.ProjectServices/CodeGeneration/CustomCodeGenerator.cs | User-editable partial class stub |
Tools/Gum.ProjectServices/CodeGeneration/CodeOutputProjectSettings.cs | Project settings classes + enums |
Tools/Gum.ProjectServices/CodeGeneration/CodeOutputElementSettings.cs | Element settings class |
Tools/Gum.ProjectServices/CodeGeneration/CodeGenerationFileLocationsService.cs | Output path resolution |
Tools/Gum.ProjectServices/CodeGeneration/CodeGenerationNameVerifier.cs | C# name compliance |
Tools/Gum.ProjectServices/CodeGeneration/VariableExclusionLogic.cs | Platform-specific variable exclusion |
Tools/Gum.Presentation/CodeOutputPlugin/CodeOutputPluginBase.cs | Plugin body shared by both heads; Gum/CodeOutputPlugin/MainCodeOutputPlugin.cs (frozen WPF) and Tool/Gum.Avalonia/Plugins/CodeOutput/ (Avalonia) are thin view heads |
Tools/Gum.Presentation/CodeOutputPlugin/Manager/CodeGenerationService.cs | Generation orchestration (headless) |
Tools/Gum.Presentation/CodeOutputPlugin/Manager/ParentSetLogic.cs | Forms parent relationship handling (headless) |
Tools/Gum.Presentation/CodeOutputPlugin/Manager/RenameService.cs | Element rename to code rename (headless) |
Exposed shader vectors -- A vector shader parameter is exposed as N saved exposures named <Base>__X to <Base>__W (ShaderVectorRow.GetExposureSuffix). CodeGenerator groups them into one write-only System.Numerics.VectorN property and the screen emits one assignment; a lone exposed component stays an ordinary float property. Keep the suffixes in step with ShaderVectorRow or older tools and runtimes stop seeing N plain exposed floats.
Engine profile switches -- IVariableRestrictions.IsElementBlocked is the one blocked-element decision (unsupported standard type, or any component while DisableComponents); every skip site (generator, headless runner, Code tab, orphan scan) asks it. DisableBehaviors makes GetFormsTypeFromBehaviors return nothing (Forms base class, Forms control member, FormsProperty assignments, default-template registration for the behavior's Forms type), so the component derives from FrameworkElement; the flag reaches static helpers as a parameter (CodeGenerationContext.BehaviorsDisabled). Hand-written GetGumFormsTypeFromBehaviors calls skip the switch.
Debug Only elements (#7200) -- CodeOutputElementSettings.DebugOnly (omitted from the .codsj while false) wraps the whole GetGeneratedCodeForElement output in #if DEBUG, and DebugOnlyElementLogic (flag follows the BaseType chain) drives GuardIfDebugOnly around every per-instance emission site (declaration, creation, parenting, variables, exposed variables, states, localization) and a separate #if DEBUG using block. A new per-instance emission site must use the guard. Toggling regenerates GetElementsToRegenerate in CodeOutputTabController.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

