Skip to content

feat(NitroInput): tabularNums for tabular digits in any font; Android edit text asks for tnum - #72

Merged
ronickg merged 3 commits into
mainfrom
fix/tabular-digit-slots
Oct 13, 2026
Merged

ronickg merged 3 commits into
mainfrom
fix/tabular-digit-slots

Conversation

@ronickg

@ronickg ronickg commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Problem

In a font with proportional digits, the keepPlaceholder tail shifted while typing. Reproduced with the example's Open Runde, which is byte-identical to Uno's:

  • Typing 1171178 into a 0000 0000 0000 mask moved the tail's end from 172.6 to 158.1 dp on Android and from 173.0 to 159.7 pt on iOS.
  • The system font (Roboto / SF) didn't move.

Open Runde ships no tnum at all; the request is open upstream as lauridskern/open-runde#10. Uno's ABC Diatype has tnum, but it's off by default, and Android's edit text never asked for it.

Changes

tabularNums prop

value what happens cost
unset (default) The font's tabular figures, where it has them (system fonts do). This is the old behaviour. none
true Every digit as wide as the widest even in a font without tabular figures (Open Runde), centred, as NitroNumber's columns are. Applies to the overlay, the field's own text, the placeholder, the tail and the JS-thread measure. a little per change, see below
false The font's proportional figures. A style's fontVariant: ['proportional-nums'] means the same. none

Natively these are two props: tabularNums (request tnum) and slotDigits (lay digits out in slots).

Android: tnum on the edit text (always on)

The overlay's paints already asked for tnum, but the edit text didn't. That alone fixes the shift for Diatype.

How the slots work

  • Android:
    • One ReplacementSpan per digit, and only the changed spans are touched per keystroke. The spans are NoCopySpan, so they never leave with copied text.
    • Letter spacing was tried first and doesn't work: from API 35, Minikin trims it at line edges (letter_spacing_justification).
  • iOS:
    • Per-character .kern, plus a first-line indent for the first digit's leading half.
    • When two neighbouring glyphs both carry a non-zero kern, the font's pair kerning comes back (Open Runde kerns "74" by −0.058 em), so that kerning is subtracted.
    • The attributed text is re-set synchronously in the edit handler: skipped during marked text, selection restored.

Verification

Tail end per keystroke with tabularNums, typing 7474 1178 (dp / pt):

field Android iOS
Open Runde Regular 181.0 ×8 180.7–181.0
Open Runde Medium / Semibold / Bold constant within 0.4
CLABE mask, 16 pt 202.7 ×8 201.7–202.0
reflow 180.6 ×8 181.0 ×8
ABC Diatype Bold, unset (Android only, trial font not committed) 181.7 ×7 –
  • Test suites:
    • Harness: iOS 152/152, Android 148/148. New tests check:
      • with tabularNums, an Open Runde mask keeps its width whatever digits are typed (plain and reflow), plus a first-layout case;
      • unset or false, digits keep their own widths.
    • Jest: 190 passed, including the prop → native and measure mapping and fontVariant. Typecheck clean.
  • Fast typing (iOS): 9 bursts of 12 digits each, no keys dropped. RN saw drops when setAttributedText: ran async on a focused field ([iOS][Fabric] TextInput drops a keystroke right after the first character typed into an empty field (_textOf:equals: compares attributes of a focused field) react/react-native#58904); ours runs inside our own edit handler.
  • Cost per keystroke with tabularNums in Open Runde:
    • Android: span update 0.19 ms median, draw +0.25 ms.
    • iOS: about 0.4 ms. When the text ends in a space (the mask's separator), UIKit's keyboard relayout on the re-set adds about 5–7 ms on the simulator.
    • Unset or false: none.

Known limits

  • With tabularNums and a non-zero letterSpacing on Android 15+, the mask's separator space still moves the tail by about 1 dp, because the platform trims that space's spacing.
  • Letters stay proportional. Only digits are tabular.

🤖 Generated with Claude Code

Ronald Goedeke and others added 3 commits October 12, 2026 23:43
…bular digits

A font with proportional digits and no tnum (Open Runde) made every typed
"1" narrower than the placeholder "0" it replaced, so keepPlaceholder's
tail slid left on each keystroke and an amount changed width with its
digits. Each digit of such a font is now laid out in a slot as wide as the
widest digit, centred, in the overlay, the field's own text, the
placeholder, the tail and the JS-thread measure:

- Android: a ReplacementSpan per digit (letter spacing can't do it: API 35+
  trims it at line edges), only the changed spans touched per keystroke.
- iOS: per-character kern plus a first-line indent, the font's pair
  kerning (which a non-zero kern brings back) taken out.

Android's edit text also asks for tnum now, as the paints did, which fixes
fonts whose tabular figures are off by default (ABC Diatype).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Default true, as on NitroNumber. false keeps each digit at the font's own
width (pnum / kProportionalNumbersSelector), for prose in a proportional
face, and skips the digit slots on both platforms. Without the prop a
style's fontVariant: ['proportional-nums'] turns it off, as on TextInput.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unset (the default) the field asks for the font's tabular figures, at no
cost, as before; true also lays out every digit as wide as the widest for a
font without them (the slots, a little work per change); false asks for the
proportional figures. Natively the two halves are tabularNums (tnum) and
slotDigits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ronickg ronickg changed the title fix(NitroInput): keepPlaceholder's tail stays put in fonts without tabular digits feat(NitroInput): tabularNums for tabular digits in any font; Android edit text asks for tnum Oct 13, 2026
@ronickg
ronickg merged commit 131ed7b into main Oct 13, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant