---
title: "UI"
description: "Canvases, elements, layout, flexbox, scaling, and hit testing — the screen-space UI system."
canonical_url: "https://docs.ctrl-neo.dev/en/neogamelib/ui"
---
# UI

NeoGameLib has its own screen-space UI: canvases full of elements, laid out
in UI units with anchors, pivots, sizing modes, padding, and flexbox. It draws
after the world pass, ignores the camera entirely, and scales with the
viewport.

## The model

- **UI units** are layout units; by default 1 UI unit = 1 viewport pixel.
- Elements form a tree under a `NeoCanvas` (the root). Canvases are the only
roots; you can't put a canvas inside another element.
- The UI bus updates canvases before gameplay each frame and draws them after
the world pass, one `SpriteBatch` per canvas.
- `NeoGame.OnDrawUI` draws afterwards in raw window pixels, always on top.

```csharp
NeoCanvas canvas = new();
NeoPanel panel = new()
{
    Transform =
    {
        Position = new Vector2(10, 10),
        Size = new Vector2(200, 100),
        Anchor = Vector2.Zero,
        Pivot = Vector2.Zero,
    },
    Color = Color.Gray,
};
NeoLabel label = new() { Text = "HP 100", Color = Color.White }; // Font is borrowed, never disposed by the UI
panel.Children.Add(label);
canvas.Children.Add(panel);
UIBus.Add(canvas);               // updates before gameplay, draws after the world
```

`UIBus` is `NeoGame.UIBus` (a `NeoUIBus`). `Add`/`Remove`/`Clear` manage
registration; a canvas in two buses at once is an error, and `dedupe: false`
lets the same canvas register multiple times in one bus. `NeoCanvas.Subscribe()` /
`Unsubscribe()` default to the singleton bus.

## Canvas

`NeoCanvas` members:

<table>
<thead>
  <tr>
    <th>
      Member
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Order
      </code>
    </td>
    
    <td>
      0
    </td>
    
    <td>
      lower order draws first
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ScaleMode
      </code>
    </td>
    
    <td>
      <code>
        ConstantPixelSize
      </code>
    </td>
    
    <td>
      how UI units map to pixels
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ScaleFactor
      </code>
    </td>
    
    <td>
      1
    </td>
    
    <td>
      <code>
        ConstantPixelSize
      </code>
      
      : viewport pixels per UI unit
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ReferenceResolution
      </code>
    </td>
    
    <td>
      <code>
        (1280, 720)
      </code>
    </td>
    
    <td>
      <code>
        ScaleWithScreen
      </code>
      
       design resolution
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Match
      </code>
    </td>
    
    <td>
      0
    </td>
    
    <td>
      <code>
        ScaleWithScreen
      </code>
      
       blend: 0 = width, 1 = height, between = log-space blend
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        PixelPerfect
      </code>
    </td>
    
    <td>
      false
    </td>
    
    <td>
      whole-number scale (min 1) + point sampling
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Scale
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      computed pixels-per-unit, updated each pass
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Children
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      element tree
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ScreenToCanvas(Vector2)
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      window pixels → UI units (letterbox + scale aware)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        CanvasToScreen(Vector2)
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      UI units → window pixels
    </td>
  </tr>
</tbody>
</table>

The canvas layout size is `viewport / Scale`, so anchors, padding, flexbox,
and `Fit` all work in UI units regardless of resolution.

### Scaling

```csharp
NeoCanvas debug = new() { ScaleFactor = 2f };   // 1 UI unit = 2 pixels

// design at 1280x720, grow/shrink with the viewport:
NeoCanvas hud = new()
{
    ScaleMode = NeoCanvasScaleMode.ScaleWithScreen,
    ReferenceResolution = new Vector2(1280, 720),
    Match = 1f,
    PixelPerfect = true,
};
```

`Match` blends in log space (Unity-style): 0.5 treats a 2x-wider viewport the
same as a 2x-taller one. With `ViewportAspect` set to the reference ratio
(e.g. 16:9 for 1280x720), every `Match` value yields the same scale.

<callout color="primary" icon="i-lucide-info">

SpriteFonts are bitmaps: text scaled way up gets blurry. Load a bigger font
for big scale ranges.

</callout>

## ScreenTransform

Every element has `element.Transform`, a `ScreenTransform`. It inherits the
world `Transform` (`Position`, `Rotation`, `Scale`, `Up`/`Down`/`Right`/`Left`),
plus:

<table>
<thead>
  <tr>
    <th>
      Member
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Size
      </code>
    </td>
    
    <td>
      <code>
        (0, 0)
      </code>
    </td>
    
    <td>
      the element's own size in UI units (used by <code>
        Fixed
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Anchor
      </code>
    </td>
    
    <td>
      <code>
        (0.5, 0.5)
      </code>
    </td>
    
    <td>
      where the element sits in its parent: 0 = left/top, 1 = right/bottom
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Pivot
      </code>
    </td>
    
    <td>
      <code>
        (0.5, 0.5)
      </code>
    </td>
    
    <td>
      its own origin point
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        WidthMode
      </code>
      
       / <code>
        HeightMode
      </code>
    </td>
    
    <td>
      <code>
        Fixed
      </code>
    </td>
    
    <td>
      sizing per axis (below)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        DesiredSize
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      computed content size (read-only)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        LayoutPosition
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      top-left of the box from the last pass (read-only)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        LayoutSize
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      box size from the last pass (read-only)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        HasVisualTransform
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      true when Rotation/Scale deviate from identity
    </td>
  </tr>
</tbody>
</table>

`Transform` is get-only; set members with the nested initializer
`Transform = { ... }`.

**Placement formula:**

```text
top-left = parent top-left + parent size * Anchor + Position - own size * Pivot
```

(For children of padded containers, "parent" is the padded inner box.)
Canvases and flexbox-managed children ignore `Anchor`/`Pivot`.

**Rotation and Scale** behave like a CSS transform: they rotate/scale the
element and its subtree around the pivot for drawing and hit testing, but
layout, `Fit`, and flexbox use the untransformed box — a rotated button does
not push its neighbors. A rotated/scaled element costs one extra
`SpriteBatch` pass for its subtree. The canvas's own rotation/scale do
nothing (use the canvas scale settings).

## Sizing modes

`NeoUISizing` per axis (`WidthMode`, `HeightMode`):

<table>
<thead>
  <tr>
    <th>
      Mode
    </th>
    
    <th>
      Behavior
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Fixed
      </code>
    </td>
    
    <td>
      use <code>
        Size
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Fill
      </code>
    </td>
    
    <td>
      take the parent's full inner length; in a flexbox, <code>
        Fill
      </code>
      
       shares leftover space on the main axis and stretches on the cross axis
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Fit
      </code>
    </td>
    
    <td>
      wrap the content: measured text for labels/buttons, or a box holding all children
    </td>
  </tr>
</tbody>
</table>

Labels and buttons default to `Fit` on both axes. To give a button a fixed
size, set the modes back to `Fixed` and set `Size`.

## Elements

### NeoPanel

A solid rectangle:

```csharp
NeoPanel card = new()
{
    Transform = { WidthMode = NeoUISizing.Fit, HeightMode = NeoUISizing.Fit },
    Color = Color.Gray,
    Padding = new NeoPadding(16, 8),   // 16 left/right, 8 top/bottom
};
```

- `Color` — tint (`Alpha = 0` skips drawing).
- `Padding` — per-side inner spacing; children anchor to the padded box,
`Fill` takes the inner size, `Fit` adds padding around children.

`NeoPadding` constructors: `(all)`, `(horizontal, vertical)`,
`(left, top, right, bottom)`; implicit from `float`; settable per side
(`new NeoPadding(8) { Bottom = 24 }`). Negative or non-finite values throw.

### NeoFlexBox

A panel that orders children in a `Row` or `Column`:

```csharp
NeoFlexBox menu = new()
{
    Direction = NeoFlexDirection.Column,
    Gap = 8, Padding = 12,
    Justify = NeoFlexAlign.Center,     // main axis
    Align = NeoFlexAlign.Stretch,      // cross axis
    Transform = { WidthMode = NeoUISizing.Fit, HeightMode = NeoUISizing.Fit },
    Color = Color.DarkSlateGray,
};
menu.Children.Add(new NeoButton { Text = "Play", Font = font });
menu.Children.Add(new NeoButton { Text = "Quit", Font = font });
```

<table>
<thead>
  <tr>
    <th>
      Member
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Direction
      </code>
    </td>
    
    <td>
      <code>
        Row
      </code>
      
       (ltr) or <code>
        Column
      </code>
      
       (ttb)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Gap
      </code>
    </td>
    
    <td>
      UI-unit gap between children on the main axis
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Justify
      </code>
    </td>
    
    <td>
      child placement on the main axis: <code>
        Start
      </code>
      
       (default), <code>
        Center
      </code>
      
      , <code>
        End
      </code>
      
      , <code>
        SpaceBetween
      </code>
      
       (extra gap distributed)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Align
      </code>
    </td>
    
    <td>
      child placement on the cross axis: <code>
        Start
      </code>
      
       (default), <code>
        Center
      </code>
      
      , <code>
        End
      </code>
      
      , <code>
        Stretch
      </code>
      
       (fill cross length), <code>
        SpaceBetween
      </code>
      
       (stretches like <code>
        Stretch
      </code>
      
      )
    </td>
  </tr>
</tbody>
</table>

`Fill`-mode children share leftover main-axis space equally (after fixed
children and gaps). Flexbox children ignore their anchors/pivots.

### NeoLabel / NeoButton

```csharp
NeoLabel label = new() { Text = "HP 100", Font = font, Color = Color.White };

NeoButton play = new()
{
    Text = "Play", Font = font,
    Transform = { Anchor = Vector2.Zero, Pivot = Vector2.Zero },
};
play.OnClick = () => StartGame();
```

- `NeoLabel`: `Text`, `Font`, `Color`. Draws at native text size — no
wrapping, scaling, or clipping.
- `NeoButton`: clickable label. `OnClick` fires when the left button is
pressed and released over the button. `HoverColor`/`PressedColor` tint the
hover/pressed states; `Hovered` is exposed. Reads the `Input` facade, so it
needs a running game.
- Both default to `Fit` sizing; the font is borrowed, never disposed.

### NeoImage

```csharp
NeoImage icon = new() { Texture = tex, Color = Color.White };
icon.Transform.Size = new Vector2(32, 32);   // scales texture to the box
```

Draws the texture (or `SourceRectangle`) stretched to the layout box.

## Custom elements

Subclass `NeoUIElement` and override:

- `OnUpdate(GameTime)` — runs every update pass while `Enabled` (subtree-gated).
- `OnDraw(SpriteBatch)` — runs every draw pass while `Visible` (batch begun,
in element space).
- `MeasureContent()` — the element's own content size; `Fit` takes the larger
of this and the children box.

```csharp
public class HealthBar : NeoUIElement
{
    public float Value;                 // 0..1
    public Texture2D Pixel;             // your own 1x1 white texture

    protected override Vector2 MeasureContent() => new(100, 8);

    protected override void OnDraw(SpriteBatch sb)
    {
        Vector2 pos = ScreenTransform.LayoutPosition;
        Vector2 size = ScreenTransform.LayoutSize;
        sb.Draw(Pixel, pos, null, Color.Red, 0, Vector2.Zero, size, SpriteEffects.None, 0);
        sb.Draw(Pixel, pos, null, Color.Green, 0, Vector2.Zero,
            new Vector2(size.X * Value, size.Y), SpriteEffects.None, 0);
    }
}
```

(`NeoPanel` uses an internal 1x1 white pixel; custom elements that draw
rectangles need their own.)

Element members you'll touch: `Children`, `Parent`, `ScreenTransform`
(=`Transform`), `Enabled` (updates), `Visible` (draws — independent of
`Enabled`), `Canvas` (walk up to the root), `VisualMatrix`.

## Hit testing

```csharp
bool over = element.ContainsScreenPoint(Input.MousePosition);  // inside the DRAWN box:
// letterbox, canvas scale, rotation, scale all handled
Vector2 local = element.ScreenToLocal(Input.MousePosition);    // window px -> layout space
Vector2 screen = canvas.CanvasToScreen(element.Transform.LayoutPosition); // UI units -> px
```

Buttons already do all of this; use `ContainsScreenPoint` for custom elements.

## Draw order within the UI

- Canvases draw in `Order` (then registration), each in its own scaled batch.
- Within a canvas, parent draws before children, children in add order.
- Rotated/scaled subtrees get their own batch pass.
- Everything UI draws after the world pass; `OnDrawUI` draws last.


## Sitemap

See the full [sitemap](https://docs.ctrl-neo.dev/sitemap.md) for all pages.
