Summary#
Layout.TextWidth tells you how wide a string will paint, before it is painted:
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#
| 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#
The size argument is optional, and omitting it is the safe form#
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#
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:
double gutter = Layout.TextWidth(longest) ?? 40.0;This is the same shape layout primitives'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#
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#
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#
Reserve exactly the gutter the longest tick label needs — and thin the labels out when they would collide:
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#
- layout primitives — the layout primitives, and the container reads (
Layout.Width/Layout.Height) this is shaped after - Canvas — drawing text yourself, where you also choose the size it is drawn at
- keys — the other primitive that answers a question about the live page