Programmatic UI

Overview

Chisel’s UI runs on Gum (via MonoGameGum) for layout and rendering. There are two ways to build UI on top of it, and which one you reach for depends on what you’re building:

Gum, directly

Screens laid out in the Gum tool (or assembled at runtime from GraphicalUiElement primitives) and shown through GumUtils. This is what the HUD, main menu, and weapon inventory grid use; anything meant to be skinnable, data-driven, or eventually handed to an artist to lay out visually.

CGUI

A small fluent builder on top of Gum’s Forms controls, purpose-built for windows you construct entirely in code; options menus, save/load dialogs, confirmation popups, debug tools.

Both ultimately produce the same underlying Gum object graph, so you can mix them. A CGUI window’s InnerPanel is just a normal ContainerRuntime you can add hand-built Gum elements to, and a Gum screen can host a CGUI-built panel as a child.

Gum: Showing and Managing Screens

Gum is initialized once in engine startup against a project file (Gum/gumproj.gumx by default), which is where screens built in the Gum designer tool live. At runtime, use the static GumUtils helper to show, check, and remove them:

var hudScreen = GumUtils.ShowScreen("PlayerHUD");

if (!GumUtils.IsScreenActive("Menu"))
    GumUtils.ShowScreen("Menu");

GumUtils.RemoveScreen("PlayerHUD");
GumUtils.HideAllScreens(); // e.g. on level transition

ShowScreen is idempotent. Calling it again on an already-active screen just returns the existing instance rather than creating a second one. GetScreen(name) retrieves an active screen’s root element without showing or hiding anything, useful for toggling visibility on something that should stay loaded:

GumUtils.GetScreen("PlayerHUD").Visible = DrawHud;

Gum: Extending a Screen at Runtime

Designer-built screens are a starting layout, it’s normal to reach into a screen’s named elements and add children programmatically for anything data-driven (a variable-length list, a grid that grows with content, etc.). This is how the weapon HUD builds its slot grid: WeaponInventory is handed the HUD screen’s root and finds a container the Gum project defines as Weapons, containing a stack panel named ColumnStack:

weaponsUIContainer = (ContainerRuntime)gumRoot.GetGraphicalUiElementByName("Weapons");
weaponsUIColumnStack = (DefaultFromFileStackPanelRuntime)weaponsUIContainer.GetChildByName("ColumnStack");

From there it builds one ContainerRuntime per weapon slot and adds each as a child of the stack panel, which handles laying them out left-to-right automatically:

var container = new ContainerRuntime();
container.Width = (1f / slots) * 100;
container.WidthUnits = Gum.DataTypes.DimensionUnitType.PercentageOfParent;
container.Height = 75;
container.HeightUnits = Gum.DataTypes.DimensionUnitType.PercentageOfOtherDimension;
container.Name = $"S{i}";
weaponsUIColumnStack.AddChild(container);

The DimensionUnitType values you’ll use most:

Absolute

A fixed pixel value.

RelativeToParent

Parent’s size plus this value. A negative number shrinks it, giving you “fill parent minus a margin”.

PercentageOfParent

Percentage of the parent’s size along the same axis.

PercentageOfOtherDimension

Percentage of the parent’s size along the other axis. This is how the weapon slot containers above stay square-ish regardless of column count.

Ratio

Share of remaining space relative to sibling elements using Ratio, similar to flexbox’s flex-grow.

Common child types you’ll build with directly: ContainerRuntime (a plain layout box), TextRuntime, SpriteRuntime (set .Texture, usually with TextureAddress = Gum.Managers.TextureAddress.EntireTexture), and ColoredRectangleRuntime (set .Color, and .Red/.Green/.Blue/.Alpha individually for cheap per-frame flash/fade effects without allocating a new color each time). Interactive elements expose a .Click event directly on the runtime, no separate Forms control required for simple cases:

rowRoot.Click += (s, e) =>
{
    // handle selection
};
Note

Prefer extending an existing designer screen over building one entirely from code when the result is meant to be visible to the player. Keep hand-rolled, fully-code-built UI (like most of CGUI below) for internal tooling and dialogs where visual polish matters less than speed of iteration.

CGUI: Quick Code-Built Windows

CGUI is a fluent builder that wraps Gum’s Forms controls (Gum.Forms.Controls) with sensible defaults and automatic vertical layout, so you can put together a functional window in a few chained calls without hand-placing every element. Start a window with CGUI.Window(title, width, height), chain content methods, and finish with .Build() to construct and open it:

CGUI.Window("Confirm", 300, 150)
    .Label("Are you sure you want to delete this save?")
    .BottomBar(
        ("Yes", (w) => { w.Close(); DoDelete(); }),
        ("No", null)
    )
    .Build();

A button callback of null just closes the window, which is why "No" above doesn’t need its own handler. Content methods stack downward automatically, each one advancing past padding and the height you gave it:

Label(text, height)

A single line of text.

TextBox(out result, placeholder, height)

A single-line text field. Returns the underlying Gum.Forms.Controls.TextBox so you can read .Text later.

List(items, out result, height) / Dropdown(items, out result, height)

A ListBox or ComboBox pre-populated with strings, returned so you can read the selection or react to .SelectionChanged.

Separator(thickness)

A thin horizontal divider line.

Space(pixels)

Adds vertical gap without placing an element.

Image(texture, height)

A sprite stretched to the window’s inner width.

Buttons((label, onClick)[]) / Button(label, onClick)

A row of one or more equal-width buttons, inline in the normal content flow.

Panel(height, build, baseColor) / ScrollPanel(height, build)

A nested sub-region. build Receives its own CGUI scoped to that region, so you compose complex layouts by nesting these.

BottomBar((label, onClick)[])

A button row docked to the bottom of the window, outside the normal top-down flow.

For a scrollable list of custom-templated rows - thumbnails, multi-line entries, anything beyond a plain string list - use FillList. It fills all remaining vertical space (minus room for a following BottomBar) and hands you a CGUIRow per item to build with .Thumbnail() and .Text():

CGUISelectableList<SaveEntry> saveList;

CGUI.Window("Load Game", 700, 500)
    .FillList(saves, (row, save) => row
        .Thumbnail(save.Screenshot, width: 96)
        .Text(save.DateDisplay, save.SaveType),
        out saveList,
        itemHeight: 72)
    .BottomBar(
        ("Load", (w) => { if (saveList.Selected != null) LoadSave(saveList.Selected.SavePath); }),
        ("Cancel", (w) => Close())
    )
    .Build();

saveList.DoubleClicked += (entry) => LoadSave(entry.SavePath);

FillList returns a CGUISelectableList<T> handle with a Selected property, a SelectionChanged event, and a DoubleClicked event.

The window itself is a real CGWindow (draggable, resizable via its border regions, with its own title bar), so once you have a reference (either the return value of .Build(), or captured via a field like mainWindow) you can call .Open(), .Close(), or subscribe to its Closed event just like any other window in the engine. Nothing stops you from building a window once in an Initialize() method and reopening the same instance later rather than rebuilding it from scratch each time.OptionsWindow does exactly this.

Note

Button callbacks registered through CGUI (Buttons, Button, BottomBar) run deferred, after the current input pass finishes, so it’s safe for a callback to close its own window or open another one without corrupting whatever loop is currently iterating over UI elements.

CGUI: Dropping to Raw Controls

When CGUI’s chained methods don’t cover what you need, build the underlying Forms control directly and place it yourself with window.InnerPanel.AddChild(...). This is how OptionsWindow handles its tab strip and per-category settings, which don’t fit a simple top-down stack. The wrapper types under Engine.UI (CGButton, CGTextBoxRuntime, CGListBox, CGComboBox, CGScrollViewer) exist purely to apply Chisel’s default styling to the stock Gum Forms controls, so reach for those instead of the bare Gum.Forms.Controls equivalents even outside the fluent builder:

var btn = new CGButton().FormsControl;
btn.Anchor(Gum.Wireframe.Anchor.TopLeft);
btn.Text = "Apply";
btn.Click += (s, e) => Apply();
mainWindow.InnerPanel.AddChild(btn);

Styling.Default18 / Styling.Default24 give you the same bitmap fonts CGUI uses internally, and Styling.Colors the shared palette, for anything you’re styling by hand. CGDarkPanel is the flat-colored background panel used throughout (backing color as a 0–255 constructor argument) if you need a plain recessed region that isn’t produced via .Panel(...).