# Layout.TextWidth

> Answers how wide a string will actually paint, measured against the font the surrounding container paints with. Null wherever there is no font to measure against — the server, and any environment without text metrics — so the app supplies its own estimate with ?? and owns the fallback.

<!-- id: ui-text-measurement · area: ui · stability: stable · html: https://osysharp.com/reference/ui/text-measurement/ -->

## Summary        {#summary}
`Layout.TextWidth` tells you **how wide a string will paint**, before it is painted:

```osy syntax
render {
  // How much room does the longest label need? Reserve exactly that, and no more.
  Axis(gutter: Layout.TextWidth(longestLabel) ?? 40.0);
}
```

Almost every sizing question a UI asks has a layout answer — a flex child fills its share, an SVG viewBox maps a
nominal drawing onto whatever box it lands in, and neither needs a number in advance. Text is the exception. Whether
two axis labels **collide**, whether a centred readout will be **clipped**, and how wide a **gutter** a value label
needs are all decided before the box those things live in exists, and flexbox has no opinion to consult.

## Signature      {#signature}

| Form | Measures at |
|---|---|
| `Layout.TextWidth(text)` | the size the container **already paints at** |
| `Layout.TextWidth(text, fontSize)` | `fontSize` pixels, in the container's family and weight |

Returns a **nullable `double`**. `text` must be a `string`.

## Description    {#description}

### The size argument is optional, and omitting it is the safe form   {#font}

The number is only as good as the font it was measured with, and a measurement taken against the *wrong* font is not
an error — it is a plausible number that lays out slightly wrong. So the platform does not take the font from you.
It reads the **computed style of the container your component renders into**: family, weight, style, and — unless you
say otherwise — size.

Pass a size only when the text will genuinely be drawn at a size the container is not using (a heading you are about
to render, a canvas label). Family and weight still come from the container even then, because those are the parts an
author could not reliably supply anyway.

### Why is the measurement null? — no font on the server   {#nullable}

There is no font on a server, so a server-rendered page measures nothing. Rather than invent a number — which would
paint a layout built on a font nobody is using — the read answers **null**, and your `??` decides:

```osy syntax
double gutter = Layout.TextWidth(longest) ?? 40.0;
```

This is the same shape [layout primitives](https://osysharp.com/reference/ui/layout/)'s container reads use, and for the same reason: a *size* has no honest reading of
"not measured", so zero would be a lie that lays out. The estimate lives in your app, where you can see it.

The first client render corrects it, and so does the moment a **web font finishes loading** — until then the browser
can only measure the fallback face, so the answer is re-taken and anything that read it re-renders. Nothing is wired
by the app.

### Can I measure text in an action? — `render` only   {#render-only}

In a `render` block only. An `action` or a function body may run on the server and runs independently of any paint,
so it has no container and no font to measure against — that is a compile error, not a runtime surprise. Read it in
`render` and pass the number in.

### What it costs   {#cost}

Nothing on a page that never calls it, and no layout pass on a page that does: the measurement reads font metrics
directly rather than laying out a hidden element, and repeated measurements of the same string in the same font are
served from a cache. An axis measuring a dozen labels on every resize is a normal thing to write.

## Examples       {#examples}

Reserve exactly the gutter the longest tick label needs — and thin the labels out when they would collide:

```osy test app=ui-text-measurement
string Longest(string[] labels) {
  var best = "";
  foreach (var l in labels) { if (l.Length > best.Length) { best = l; } }
  return best;
}

// How many categories to skip so the widest label fits its slot. At least 1 — a step of 0 would show nothing.
int Step(double widest, double slot) {
  var needed = (int)(widest / slot) + 1;
  return needed < 1 ? 1 : needed;
}

[Page("/chart")]
[Render(CSR)]
[AllowAnonymous]
component MiniAxis() {
  string[] labels = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"];

  render {
    // The gutter is exactly what the longest label needs — measured, not guessed at 40.
    var gutter = Layout.TextWidth(Longest(labels)) ?? 40.0;
    // Each category gets an equal slice of the plot. If the widest label does not fit its slice, show every Nth.
    var slot = ((Layout.Width ?? 600) - gutter) / labels.Length;
    var step = Step(Layout.TextWidth(Longest(labels)) ?? 40.0, slot);

    Row(gap: 1) {
      Box(w: gutter + "px") { Text("value"); }
      Stack {
        foreach (var i in Enumerable.Range(0, labels.Length)) {
          if (i % step == 0) { Text(labels[i]); }
        }
      }
    }
  }
}
```

The `?? 40.0` is not defensive clutter — it is what paints on the server and in the instant before the browser has
measured, so choose a number that looks right rather than a zero.

## See also   {#see-also}
- [layout primitives](https://osysharp.com/reference/ui/layout/) — the layout primitives, and the container reads (`Layout.Width` / `Layout.Height`) this is shaped after
- [Canvas](https://osysharp.com/reference/ui/canvas/) — drawing text yourself, where you also choose the size it is drawn at
- [keys](https://osysharp.com/reference/ui/keys/) — the other primitive that answers a question about the live page
