Mega UI
Widgets
Everything you can stack on a screen, and the rule that decides where state lives.
The rule for all of them
A Toggle asks you whether it is on, and tells you when it is clicked. It does not remember. That is why a screen can never drift out of step with the value it shows — there is only one copy, and it is yours.
The practical shape of that: anything variable is a Func, and it is called again
every time the window opens. And every colour is optional — see
Look and feel.
Text
Header
ui.Header("Warehouse");A section title — bigger, brighter, with room above it.
Label
ui.Label("Everything currently in storage.");A line of plain text that never changes.
Value
ui.Value(() => $"{Warehouse.Count} boxes");Text that is read again on every open. This is the one you want for anything live.
It runs on the main thread, on open, and nothing is cached between calls. A database round trip in there is a hitch the player feels.
Note
ui.Note("Staff refill a shelf as soon as it drops below this.");Small dim text, indented, for the line that explains the control above it. It is what stops a settings screen from being a column of unexplained switches.
Controls
Button
ui.Button("Order more", Warehouse.Order);Full width when it stacks; sized to its text inside a row. The label shrinks to fit rather than running past the edge.
If your click handler throws, the exception is logged and swallowed. That is not politeness: an exception escaping into the game's EventSystem takes down click handling for the whole scene, and the player loses the mouse everywhere — not just in your app.
Toggle
ui.Toggle("Warn me about things",
() => Config.Warn,
v => Config.Warn = v);A checkbox. The getter runs on open, the setter on every click.
Stepper
ui.Stepper("How many at most", 1, 20,
() => Config.Limit,
v => Config.Limit = v);A whole number with minus and plus buttons. The value handed to your setter is already clamped to the range, so you never have to check it. Pass the bounds in either order — they are sorted for you.
Slider
ui.Slider("Spawn rate", 0.5f, 3f,
() => Config.Rate,
v => Config.Rate = v,
format: "0.0"); // whole: true snaps to integersA continuous value with the current reading beside it. Use it where the exact number matters less
than the feel of it; use a Stepper where the player is going to want 7.
Dropdown
ui.Dropdown("Difficulty", new[] { "Relaxed", "Normal", "Brutal" },
() => Config.Mode,
i => Config.Mode = i);One of a short list. The list opens above the rest of the window, so it is not clipped by the card it sits in.
TextField
ui.TextField("Shop name", () => Config.Name, v => Config.Name = v,
placeholder: "Untitled");While the field has focus the keyboard belongs to it, so writing "Wagon" does not walk the player across the shop and open their inventory halfway through the word. Your setter is called when the edit ends, not on every keystroke — otherwise you would be handed "W", "Wa", "Wag" and would save all three.
Showing numbers
Stats
ui.Stats(
("Revenue today", () => "$" + Day.Revenue.ToString("N0")),
("Customers", () => Day.Customers.ToString()),
("Items sold", () => Day.Items.ToString()));A row of big numbers with small captions. Put it at the top: it answers "how is it going" before the player reads a word.
ProgressBar
ui.ProgressBar("Restocking", () => Restock.Progress,
text: () => $"{Restock.Done} of {Restock.Total}");The fraction is clamped to 0–1 for you.
Image
ui.Image(builtinIcon: "truck", height: 90f);
ui.Image(png: MyArt.LogoBytes, height: 140f);A built-in icon or your own PNG, full width, at the height you ask for. Aspect is preserved, so a wide image is centred rather than stretched.
Structure
Card
ui.Card(c => { c.Header("Today"); c.Label("..."); });A panel that sizes itself to its contents. Everything works inside: rows, headers, even another card. See Rows and cards.
Columns
ui.Columns(
left => { left.Header("Stock"); left.Value(() => "..."); },
right => { right.Header("Staff"); right.Value(() => "..."); });Side by side, sharing the width evenly. Each column is a full builder, and both end up as tall as the taller one.
Tabs
ui.Tabs(new[] { "You", "Staff" }, () => _tab, i => { _tab = i; app.Refresh(); });
if (_tab == 0) ui.Label("Your upgrades");
else ui.Label("Staff upgrades");Mega UI draws them and reports the click; you decide what each tab shows, by checking the index while you build.
List
ui.List(new[] { "Bakery", "Butchery", "Drinks" },
() => _picked, i => { _picked = i; app.Refresh(); },
icons: new[] { "package", "package", "package" });Separator and Gap
ui.Separator();
ui.Gap(); // 12 pixels
ui.Gap(40f);Row widgets
Inside ui.Row(r => ...) the set is smaller, because a row is for one line of
related things rather than a whole section.
| Call | What it is |
|---|---|
r.Label(text) | Text, sized to what it contains |
r.Value(func) | Text re-read on every open, resized as it changes |
r.Button(text, action) | A button sized to its text |
r.Icon(name) | A built-in icon at row height |
r.Spacer() | Pushes everything after it to the right edge |
r.Gap(px) | Blank horizontal space |
When stacking is not enough
A tree, a map, a flow chart or a whole window of your own does not stack. That is
free positioning — still no RectTransform, but you say
where things go.
ui.Diagram(400f, d =>
{
d.Link(300, 60, 300, 140);
d.Node(300, 40, "Root", "git-branch", owned: true);
d.Node(220, 160, "Left", "zap");
});And the last resort
ui.Raw(height) hands you a positioned rectangle of the right width and gets out of
the way. What you put in it is yours, and the layout keeps flowing underneath because the height was
already counted.
No widget set covers everything, and a library that traps you is worse than no library — you uninstall it and go back to writing RectTransforms by hand. Raw is there so that never happens. But every use of it is a widget that should exist: tell me which one.