Mega UI
Free positioning
Trees, maps and flow charts - and windows bigger than the window. Still no RectTransform.
When a stack is the wrong shape
Rows and cards cover most screens. Some things are not a list at any angle — an upgrade tree, a shop floor plan, a node graph — and for those you have to say where things go.
ui.Diagram gives you a surface to do that on, and keeps drawing everything for you.
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");
d.Node(380, 160, "Right", "hand");
});The origin is the top-left, x to the right and y down — the same convention as every layout tool. Raw Unity is the other way up, and that inversion is one of the things this saves you from. The conversion happens once, inside the library.
The pieces
| Call | What it draws |
|---|---|
Node(x, y, ...) | A disc with a ring, an icon, and a small level inside it |
Link(x1, y1, x2, y2, ...) | A connector. Straight lines join cleanly; diagonals are drawn rotated |
Text(x, y, ...) | Free text, left, centred or right of the point |
Box(x, y, w, h, ...) | A filled rectangle — a background, a divider, a bar |
Button(x, y, w, h, ...) | A button at an exact spot: a tab, a toolbar entry, a close box |
Image(x, y, w, h, ...) | A built-in icon or your own PNG |
Region(x, y, w, h, ...) | A window inside the window — see below |
Raw(x, y, w, h) | A positioned rectangle. What goes in it is yours |
d.Size is the surface in pixels, so you can centre things without measuring, and
d.Theme is there for anything you colour yourself.
Nodes have more than two states
owned: true is the shortcut, and it covers a lot. A real tree has more —
bought, next in line, locked, cannot afford, coming soon — and each wants its own outline and
tint. Say so explicitly and the shortcut steps aside:
d.Node(x, y,
icon: "zap",
size: 54f,
tier: "III",
tooltip: "Sprint Speed III\nRun faster when you hold sprint",
onClick: () => Buy(u, 3),
ringColor: bought ? accent : next ? Fade(accent, 0.6f) : dim,
fillColor: bought ? Blend(card, accent, 0.12f) : next ? card : chip,
iconColor: bought ? accent : next ? textDim : Fade(textMuted, 0.55f),
tierColor: bought ? Fade(accent, 0.7f) : textMuted);A node is an opaque disc, so it hides the line running underneath it — and that is what makes a branch arrive at a node instead of crossing it. Draw the links, then the nodes, and the joins close by themselves.
Regions: bigger than the window
A tree does not fit in a shop monitor. Region is a rectangle that clips
what you draw in it, so the part that does not fit is cut at the edge instead of spilling over your
own header and footer.
d.Region(500, 260, 1000, 400, // where it is, and how much is visible
inner => DrawTree(inner), // the drawing, in content coordinates
contentWidth: 1400f, // how big the drawing actually is
contentHeight: 900f,
pan: true,
background: treeBg);With pan: true the player gets:
| Gesture | What it does |
|---|---|
| Drag | Moves the drawing, exactly as far as the pointer moved |
| Wheel | Scrolls it up and down |
| Ctrl + wheel | Zooms, around the pointer rather than around a corner |
| Ctrl + middle click | Puts it all back where it started |
The drawing cannot be dragged out of sight — its own edges are the limit — so there is no way to end up staring at an empty rectangle wondering what happened.
Every number is yours
The defaults suit an upgrade tree. A floor plan wants to pull much further back; a flow chart may want no zoom at all. None of these are baked in:
d.Region(x, y, w, h, Draw,
contentWidth: cw, contentHeight: ch, pan: true,
zoomMin: 0.25f, // default 0.6
zoomMax: 4f, // default 1.8
zoom: 0.8f, // where it starts, and where Ctrl+MMB returns to
zoomStep: 0.1f, // per wheel notch, as a fraction
wheelStep: 120f); // pixels per notch when Ctrl is not heldA tooltip on a node inside a region is drawn at window level, not inside the region, so it is never cut in half by the edge. It follows its node while you pan and zoom.
Owning the whole window
Ask for UiChrome.None, pass bleed: true, and give the diagram
ui.Area.y: the surface is now the entire window, edge to edge, and the header, the tabs
and the footer are all yours to draw.
// In your AppDefinition
Chrome = UiChrome.None,
// And in Build
ui.Diagram(ui.Area.y, d =>
{
float w = d.Size.x, h = d.Size.y;
d.Box(w * 0.5f, 31f, w, 62f, headerBg);
d.Text(20f, 24f, "MY APP", textMain, 26f, bold: true, align: UiAlign.Left);
d.Button(w - 34f, 31f, 34f, 30f, "X", () => app.Close(), card, textDim);
d.Region(w * 0.5f, (62f + h - 44f) * 0.5f, w, h - 106f,
inner => DrawContent(inner), pan: true);
d.Box(w * 0.5f, h - 22f, w, 44f, headerBg);
}, bleed: true);bleed drops the side margin the rest of the screen keeps. Without it a header bar
stops 28 px short of the frame, which does not read as a header bar.
Text on a free surface
A point can mean three different things, so say which:
d.Text(20f, y, "MEGA TREE", textMain, 26f, bold: true, align: UiAlign.Left);
d.Text(w/2f, y, "centred by default");
d.Text(w-15, y, "$9,593", textMain, 17f, bold: true, align: UiAlign.Right);
// A long status line that must not run past its box
d.Text(15f, y, controlsHint, textDim, 16f,
align: UiAlign.Left, width: w * 0.82f, shrink: true);This is a TextMeshPro behaviour worth knowing about: with the default overflow mode, text too big for its box is not clipped and not ellipsised — it simply does not render. Mega UI works around it for you (free text overflows, button labels shrink), which is why a roman numeral inside a node and a "+" on a small button both appear. If you are drawing your own through Raw, this is the one to remember.