Utility for computing how many pixels a string will occupy in Inter Regular and selecting the largest whole-pixel font size that fits a fixed width. It uses a precomputed advance table for ASCII 32-126 and heuristics for other Unicode ranges to estimate width without layout.
using System;
namespace NZombies;
/// <summary>
/// A player's name at the biggest size that fits its row (the user, 2026-10-05: *"if the name is too long, the font size
/// decreases so it always fits"*). The lobby's player list asks it for every name. Anything else that draws a name in a fixed
/// width can ask it too.
///
/// ⚠️ FROM INTER'S OWN WIDTHS: NOT A GUESS, AND NOT A MEASUREMENT AFTER LAYOUT. A panel can't say how wide its text is until it
/// has been laid out, and a size chosen afterwards would draw every long name too big for a frame first. The table is Inter
/// Regular's advance for ASCII 32–126, in thousandths of an em, read from s&box's `core/fonts/Inter-Regular.ttf`. Summed,
/// it matched the font's own measure to within 0.3 px on a 32-letter name at 16 px. Anything outside ASCII counts as a full
/// em (CJK, kana, hangul, emoji) or two thirds of one (accented Latin, Cyrillic, Greek…), which errs small rather than
/// spilling.
/// </summary>
public static class NameFit
{
static readonly short[] Inter =
{
281, 278, 403, 631, 638, 813, 639, 221, 363, 363, 500, 659, 280, 460, 276, 357, 626, 465, 605,
636, 643, 607, 624, 571, 616, 624, 276, 280, 659, 660, 659, 508, 936, 676, 651, 727, 719, 598,
587, 744, 740, 264, 542, 652, 562, 889, 753, 762, 635, 762, 639, 638, 641, 741, 676, 949, 642,
664, 625, 362, 357, 363, 470, 453, 497, 564, 621, 558, 621, 582, 361, 610, 592, 237, 237, 544,
238, 870, 586, 596, 609, 610, 372, 524, 364, 582, 557, 813, 540, 557, 542, 362, 327, 362, 660,
};
/// <summary>How wide this text is in Inter Regular at this size, in px.</summary>
public static float Width( string text, float size )
{
if ( string.IsNullOrEmpty( text ) ) return 0f;
var em = 0f;
foreach ( var ch in text )
{
if ( ch >= 32 && ch <= 126 ) em += Inter[ch - 32];
else if ( char.IsLowSurrogate( ch ) ) continue; // the pair's width went on its first half
else if ( char.IsHighSurrogate( ch ) || ch >= 0x2E80 ) em += 1000; // emoji, CJK, kana, hangul: a full em
else em += 667; // accented Latin, Cyrillic, Greek…: two thirds
}
return em * size / 1000f;
}
/// <summary>
/// The biggest whole px size, from <paramref name="max"/> down to <paramref name="min"/>, at which this text fits in
/// <paramref name="room"/> px.
///
/// ⚠️ 3% IN HAND for the renderer's hinting, which the table can't see. ⚠️ THE FLOOR IS THERE FOR A NAME NO SIZE COULD SAVE:
/// at 8 px even 32 W's, Steam's longest name in its widest letter, take 243 px, inside the lobby's 269.
/// </summary>
public static int Size( string text, float room, int max = 16, int min = 8 )
{
var at = Width( text, max ) * 1.03f;
if ( at <= room ) return max;
return Math.Clamp( (int)MathF.Floor( max * room / at ), min, max );
}
}