diff --git a/docs/content/nitro-input-props.mdx b/docs/content/nitro-input-props.mdx
index ad7e651..678258a 100644
--- a/docs/content/nitro-input-props.mdx
+++ b/docs/content/nitro-input-props.mdx
@@ -147,6 +147,10 @@ Alignment of `suffix` only.
Points added after every glyph, like `Text`'s `letterSpacing` (negative tightens). A smaller prefix or suffix gets it in proportion to its size.
+
+Unset: the font's tabular figures, where it has them (the system fonts do), at no cost. `true`: every digit as wide as the widest even in a font without them (Open Runde), laid out that wide and centred, so typing over a `keepPlaceholder` mask never moves what is still to come and an amount keeps its width as its digits change; that layout costs a little on each change. `false`: the font's proportional figures. A style's `fontVariant: ['proportional-nums']` is `false` too, as on a `TextInput`; the prop wins.
+
+
Points between the prefix and the text, and between the text and the suffix, in place of the letter spacing there.
diff --git a/example/__tests__/nitro-input-sizing.harness.tsx b/example/__tests__/nitro-input-sizing.harness.tsx
index ddca70f..122287d 100644
--- a/example/__tests__/nitro-input-sizing.harness.tsx
+++ b/example/__tests__/nitro-input-sizing.harness.tsx
@@ -44,6 +44,7 @@ const CASES: [string, NitroInputProps][] = [
['maxLength', { defaultValue: 'abcdefghij', maxLength: 4, autoWidth: true, fontSize: 20 }],
['a line height and a weight', { defaultValue: 'Tall', lineHeight: 40, fontWeight: '600', autoWidth: true, fontSize: 22 }],
['a custom font', { fontFamily: 'OpenRunde-Semibold', defaultValue: '$1,250.50', autoWidth: true, fontSize: 16 }],
+ ['a mask in a font without tabular figures', { mode: 'mask', mask: '[0000] [0000]', keepPlaceholder: true, placeholder: '0123 4567', defaultValue: '11', fontFamily: 'OpenRunde-Regular', tabularNums: true, autoWidth: true, fontSize: 20 }],
]
type Rect = { width: number; height: number }
@@ -216,3 +217,57 @@ describe('a size TextInput would give', () => {
expect(b.height - a.height).toBeCloseTo(8, 0)
})
})
+
+describe('digits in a font without tabular figures', () => {
+ // Open Runde has proportional digits and no tabular ones to ask for: a "1"
+ // is far narrower than a "0". With `tabularNums` each digit is laid out as
+ // wide as the widest, so typing over a kept placeholder never moves what is
+ // still to come, and an amount keeps its width as its digits change.
+ for (const transition of ['none', 'reflow'] as const) {
+ it(`keep a mask's width whatever is typed (${transition})`, async () => {
+ const ref = createRef()
+ const { state, Box } = boxed()
+ await render(
+
+
+
+ )
+ await waitFor(() => expect(state.mounted).not.toBeNull())
+ await sleep(SETTLE_MS)
+ const empty = rectOf(state.view!).width
+ for (const typed of ['1', '1111 1111', '7171 1', '8888 8888 8888', '1111 1111 1111']) {
+ ref.current!.setText(typed)
+ await sleep(200)
+ expect([typed, rectOf(state.view!).width]).toEqual([typed, empty])
+ }
+ })
+ }
+
+ for (const [name, tabularNums] of [['unset', undefined], ['false', false]] as const) {
+ it(`leave each digit its own width with tabularNums ${name}`, async () => {
+ const ref = createRef()
+ const { state, Box } = boxed()
+ await render(
+
+
+
+ )
+ await expectFinalAtMount(state)
+ const zeros = rectOf(state.view!).width
+ ref.current!.setText('1111')
+ // Open Runde's "1" is about three quarters of its "0".
+ await waitFor(() => expect(rectOf(state.view!).width).toBeLessThan(zeros - 8))
+ })
+ }
+})
diff --git a/packages/react-native-nitro-input/CHANGELOG.md b/packages/react-native-nitro-input/CHANGELOG.md
index 035f28e..22f2095 100644
--- a/packages/react-native-nitro-input/CHANGELOG.md
+++ b/packages/react-native-nitro-input/CHANGELOG.md
@@ -1,5 +1,21 @@
# Changelog
+## Unreleased
+
+- **`NitroInput`'s `tabularNums` prop: tabular digits in any font.** Digits
+ were tabular only in fonts that ship tabular figures: in one without (Open
+ Runde) a typed "1" was far narrower than the placeholder's "0" it replaced,
+ so `keepPlaceholder`'s tail slid left on each keystroke (by 14 dp over a
+ card number), and an amount changed width as its digits did.
+ `tabularNums` lays every digit out as wide as the widest, centred, as
+ `NitroNumber`'s columns are: in the field's own text, the placeholder, the
+ tail and the measure, on both platforms. Unset, the field asks for the
+ font's tabular figures as before, at no cost; `false` asks for its
+ proportional ones (as does a style's `fontVariant: ['proportional-nums']`).
+- **Android: a plain field's text and placeholder ask for tabular figures.**
+ The overlay's paints did, the edit text did not, so a font whose tabular
+ figures are off by default (ABC Diatype) shifted the tail the same way.
+
## 0.4.0
- **Android: `NitroNumber`'s numeric transition no longer blocks the UI
diff --git a/packages/react-native-nitro-input/README.md b/packages/react-native-nitro-input/README.md
index 750a249..6d57568 100644
--- a/packages/react-native-nitro-input/README.md
+++ b/packages/react-native-nitro-input/README.md
@@ -521,6 +521,7 @@ screen behaves like theirs with these settings:
| `affixAlign` | `'baseline' \| 'center' \| 'top' \| 'bottom'` | `'baseline'` | How they line up with the text: `top` pins glyph tops, `bottom` the bottom of the ink (a currency code sits on the digits' baseline). |
| `prefixAlign` / `suffixAlign` | same | `affixAlign` | Per-affix override. |
| `letterSpacing` | number | `0` | Points added after every glyph, like `Text`'s; a smaller affix gets it in proportion to its size. |
+| `tabularNums` | boolean | unset | Unset: the font's tabular figures where it has them (the system fonts do), at no cost. `true`: every digit as wide as the widest even in a font without them (Open Runde), laid out that wide and centred, so a `keepPlaceholder` tail never moves and an amount keeps its width as it changes; a little work on each change. `false`: the font's proportional figures, as is a style's `fontVariant: ['proportional-nums']`. |
| `prefixSpacing` / `suffixSpacing` | number | the letter spacing | Points between the prefix and the text, and between the text and the suffix. |
| `prefixOffset` / `suffixOffset` | number | `0` | Points an affix is moved down after its alignment (negative: up). |
| `format` | `NumberFormat` | none | The amount follows it: prefix and suffix, separators, fraction digits, where the sign goes. Sets `mode="number"`; the individual props override it. |
@@ -587,7 +588,7 @@ screen behaves like theirs with these settings:
| `onKeyPress` | `(event) => void` | – | Before the text changes: the character, `'Backspace'` or `'Enter'`. |
| `onNativeRef` | `(ref) => void` | – | Receives the Nitro object on mount. |
| `id` / `aria-label` | `string` | – | React Native's HTML-style aliases for `nativeID` / `accessibilityLabel`, resolved here because a Nitro view is handed the raw props; each wins over the older spelling. |
-| `style`, `testID`, … | `ViewProps`; `style` as `TextInput`'s | – | Regular view props. `testID` and the accessibility label are forwarded to the system field, the element VoiceOver, TalkBack and e2e tools interact with. The field takes its width from its parent like a `TextInput`; see `autoWidth`. As on a `TextInput`, `style` can carry the typography (`fontSize`, `fontWeight`, `fontFamily`, `color`, `letterSpacing`, `lineHeight`, `textAlign`, `textAlignVertical`; the props of the same name win), and its padding and border inset the text and add to a size the field works out for itself. Two deliberate differences: digits are always tabular (Android's Roboto has them anyway, and a field keeps its width whether it reflows or not), and Android gets no default padding (iOS's `TextInput` has none; React Native's Android one keeps the platform field's). |
+| `style`, `testID`, … | `ViewProps`; `style` as `TextInput`'s | – | Regular view props. `testID` and the accessibility label are forwarded to the system field, the element VoiceOver, TalkBack and e2e tools interact with. The field takes its width from its parent like a `TextInput`; see `autoWidth`. As on a `TextInput`, `style` can carry the typography (`fontSize`, `fontWeight`, `fontFamily`, `color`, `letterSpacing`, `lineHeight`, `textAlign`, `textAlignVertical`; the props of the same name win), and its padding and border inset the text and add to a size the field works out for itself. Two deliberate differences: digits are tabular by default where the font has tabular figures (Android's Roboto has them anyway, and a field keeps its width whether it reflows or not; see `tabularNums` for a font without them), and Android gets no default padding (iOS's `TextInput` has none; React Native's Android one keeps the platform field's). |
`keyboardType`: `'default' | 'number-pad' | 'decimal-pad' | 'numeric' | 'email-address' | 'phone-pad' | 'url' | 'ascii-capable' | 'numbers-and-punctuation'`.
diff --git a/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/DigitSlots.kt b/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/DigitSlots.kt
new file mode 100644
index 0000000..68c47fd
--- /dev/null
+++ b/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/DigitSlots.kt
@@ -0,0 +1,143 @@
+package com.margelo.nitro.nitroinput
+
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.graphics.Typeface
+import android.text.NoCopySpan
+import android.text.Spannable
+import android.text.TextPaint
+import android.text.style.ReplacementSpan
+import kotlin.math.ceil
+
+private const val DIGITS = "0123456789"
+
+/**
+ * Tabular figures for a font that has none.
+ *
+ * The paints ask for `tnum`, but a font without the feature (Open Runde) keeps
+ * its proportional digits: every typed "1" was narrower than the placeholder's
+ * "0" it replaced, so `keepPlaceholder`'s tail slid left on each keystroke and
+ * an amount changed width as its digits did. With `tabularNums` every digit
+ * is given a slot as wide as the widest digit instead, centred in it, as
+ * NitroNumber's columns are. In a font with tabular figures the digits are
+ * all that wide already, and nothing changes.
+ *
+ * In the edit text a slot is a [ReplacementSpan], whose width is a whole
+ * pixel, so a slot is the widest digit and one letter spacing rounded up, and
+ * [InputFonts] measures and draws with that same width. (Letter spacing on the
+ * digit was the first try: from API 35 the platform trims it at either end of
+ * a line, so the first and last digit went without.)
+ */
+internal object DigitSlots {
+ fun isDigit(codePoint: Int): Boolean = codePoint in '0'.code..'9'.code
+
+ /** Each digit's advance in a paint, letter spacing aside, and the widest of them. */
+ class Digits(val widths: FloatArray) {
+ val widest = widths.max()
+
+ /** A slot in px, for a letter spacing of [spacing] px. */
+ fun slot(spacing: Float): Int = ceil(widest + spacing - 0.001f).toInt()
+ }
+
+ /**
+ * The digits of [paint] (its letter spacing aside), or null when they are
+ * one width already (within a hundredth of the font size).
+ */
+ fun measure(paint: Paint): Digits? {
+ val bare = Paint(paint)
+ bare.letterSpacing = 0f
+ val widths = FloatArray(10) { bare.measureText(DIGITS, it, it + 1) }
+ if (widths.max() - widths.min() <= paint.textSize * 0.01f) return null
+ return Digits(widths)
+ }
+
+ private data class Key(val typeface: Typeface?, val size: Float, val features: String?)
+
+ // For the edit text's paint, which the spans are handed a copy of on every layout pass.
+ private val cache = HashMap()
+
+ private fun digits(paint: Paint): Digits? = synchronized(cache) {
+ val key = Key(paint.typeface, paint.textSize, paint.fontFeatureSettings)
+ if (key in cache) cache[key] else measure(paint).also {
+ if (cache.size > 32) cache.clear()
+ cache[key] = it
+ }
+ }
+
+ private fun spacing(paint: Paint): Float = paint.letterSpacing * paint.textSize
+
+ /**
+ * Re-spans every digit of [text] for [paint] (the edit text's), or only
+ * clears the spans when its font has tabular figures, or when [off].
+ */
+ fun apply(text: Spannable, paint: Paint, off: Boolean) {
+ val on = !off && paint.textSize > 0f && digits(paint) != null
+ // Only what changed: every span set or removed lays the edit text out
+ // again, and a keystroke adds one digit (the spans of the others move
+ // with their characters). Re-spanning the lot cost ~1.5 ms a keystroke.
+ val spanned = BooleanArray(text.length)
+ for (span in text.getSpans(0, text.length, Slot::class.java)) {
+ val start = text.getSpanStart(span)
+ val keep = on && text.getSpanEnd(span) == start + 1 && isDigit(text[start].code) && !spanned[start]
+ if (keep) spanned[start] = true else text.removeSpan(span)
+ }
+ if (!on) return
+ for (i in text.indices) {
+ if (!spanned[i] && isDigit(text[i].code)) text.setSpan(Slot(), i, i + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
+ }
+ }
+
+ /**
+ * Draws [text] at [x] in [paint] as the spans lay it out in the edit text:
+ * digits in their slots, the runs between them whole.
+ */
+ fun draw(canvas: Canvas, text: String, x: Float, y: Float, paint: Paint) {
+ val digits = digits(paint)
+ if (digits == null) {
+ canvas.drawText(text, x, y, paint)
+ return
+ }
+ val slot = digits.slot(spacing(paint))
+ val spacing = paint.letterSpacing
+ var left = x
+ var start = 0
+ while (start < text.length) {
+ if (isDigit(text[start].code)) {
+ paint.letterSpacing = 0f
+ canvas.drawText(text, start, start + 1, left + (slot - digits.widths[text[start] - '0']) / 2f, y, paint)
+ paint.letterSpacing = spacing
+ left += slot
+ start++
+ continue
+ }
+ var end = start + 1
+ while (end < text.length && !isDigit(text[end].code)) end++
+ canvas.drawText(text, start, end, left, y, paint)
+ left += paint.measureText(text, start, end)
+ start = end
+ }
+ }
+
+ /**
+ * One digit, drawn centred in its slot. Not copied with the text (the
+ * clipboard, a saved state): the watcher puts the spans back on whatever
+ * text the field ends up with.
+ */
+ private class Slot : ReplacementSpan(), NoCopySpan {
+ private val bare = TextPaint()
+
+ override fun getSize(paint: Paint, text: CharSequence, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
+ if (fm != null) paint.getFontMetricsInt(fm)
+ val digits = digits(paint) ?: return ceil(paint.measureText(text, start, end)).toInt()
+ return digits.slot(spacing(paint))
+ }
+
+ override fun draw(canvas: Canvas, text: CharSequence, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
+ bare.set(paint)
+ bare.letterSpacing = 0f
+ val digits = digits(paint)
+ val inset = if (digits == null) 0f else (digits.slot(spacing(paint)) - digits.widths[text[start] - '0']) / 2f
+ canvas.drawText(text, start, end, x + inset, y.toFloat(), bare)
+ }
+ }
+}
diff --git a/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/HybridNitroInputView.kt b/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/HybridNitroInputView.kt
index 87a0b2d..0ec0695 100644
--- a/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/HybridNitroInputView.kt
+++ b/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/HybridNitroInputView.kt
@@ -129,6 +129,10 @@ class HybridNitroInputView(private val context: ThemedReactContext) : HybridNitr
set(v) { field = v; markConfigDirty() }
override var textLetterSpacing: Double = 0.0
set(v) { field = v; markConfigDirty() }
+ override var tabularNums: Boolean = true
+ set(v) { field = v; markConfigDirty() }
+ override var slotDigits: Boolean = false
+ set(v) { field = v; markConfigDirty() }
override var prefixSpacing: Double = Double.POSITIVE_INFINITY
set(v) { field = v; markConfigDirty() }
override var suffixSpacing: Double = Double.POSITIVE_INFINITY
@@ -423,6 +427,8 @@ class HybridNitroInputView(private val context: ThemedReactContext) : HybridNitr
suffixFontSize = Double.NaN
signPlacement = NitroInputSignPlacement.BEFOREAFFIX
textLetterSpacing = 0.0
+ tabularNums = true
+ slotDigits = false
prefixSpacing = Double.POSITIVE_INFINITY
suffixSpacing = Double.POSITIVE_INFINITY
prefixOffset = 0.0
diff --git a/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/InputFonts.kt b/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/InputFonts.kt
index 525f6b7..4129303 100644
--- a/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/InputFonts.kt
+++ b/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/InputFonts.kt
@@ -86,9 +86,54 @@ internal class InputFonts(
InputRole.SUFFIX -> suffix
}
- /** Advance of one character, in px. */
+ /**
+ * The body's digits when they are slotted (`tabularNums` set) and the font
+ * has no tabular figures: each is then laid out in a slot as wide as the
+ * widest ([DigitSlots]); null otherwise. The affixes keep their own: they
+ * never change under the typing.
+ */
+ private val bodyDigits: DigitSlots.Digits? = if (t.slotDigits) DigitSlots.measure(body) else null
+
+ /** A slotted body digit's advance, its letter spacing aside: the edit text's slot less that spacing. */
+ private val bodySlot: Float = bodyDigits?.let { it.slot(bodySpacing) - bodySpacing } ?: 0f
+
+ private fun slotted(codePoint: Int, role: InputRole): Boolean =
+ role == InputRole.BODY && bodyDigits != null && DigitSlots.isDigit(codePoint)
+
+ /** Advance of one character, in px: a slotted digit's is the slot's. */
fun width(codePoint: Int, role: InputRole): Float =
- widthCaches[role.ordinal].getOrPut(codePoint) { paint(role).measureText(charString(codePoint)) }
+ widthCaches[role.ordinal].getOrPut(codePoint) {
+ if (slotted(codePoint, role)) bodySlot else paint(role).measureText(charString(codePoint))
+ }
+
+ /** How far to draw a character in from the left of its advance: a slotted digit is centred in it. */
+ fun inset(codePoint: Int, role: InputRole): Float {
+ if (!slotted(codePoint, role)) return 0f
+ return (bodySlot - bodyDigits!!.widths[codePoint - '0'.code]) / 2f
+ }
+
+ /**
+ * [text] in the body paint as the edit text lays it out, without letter
+ * spacing: whole, or, when its digits are slotted, in runs split at each
+ * digit, which is where the slots' spans split it.
+ */
+ fun plainWidth(text: String): Float {
+ if (bodyDigits == null) return body.measureText(text)
+ var total = 0f
+ var start = 0
+ while (start < text.length) {
+ if (DigitSlots.isDigit(text[start].code)) {
+ total += width(text[start].code, InputRole.BODY)
+ start++
+ continue
+ }
+ var end = start + 1
+ while (end < text.length && !DigitSlots.isDigit(text[end].code)) end++
+ total += body.measureText(text, start, end)
+ start = end
+ }
+ return total
+ }
/** Sum of the per-character advances of [text] (what the engine lays out). */
fun width(text: String, role: InputRole): Float {
@@ -174,9 +219,9 @@ internal class InputFonts(
paint.typeface = makeTypeface(t)
paint.textSize = sizeDp * density
paint.color = t.color ?: defaultColor
- // Tabular figures in every mode, as on iOS: the engine slides digit runs
- // in text mode too, and proportional digits would jitter as they change.
- paint.fontFeatureSettings = "tnum"
+ // Tabular figures unless asked not to, as on iOS: the engine slides digit
+ // runs in text mode too, and proportional digits would jitter as they change.
+ paint.fontFeatureSettings = if (t.tabularNums) "tnum" else "pnum"
return paint
}
diff --git a/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/InputSizing.kt b/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/InputSizing.kt
index bf34c3d..48e5376 100644
--- a/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/InputSizing.kt
+++ b/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/InputSizing.kt
@@ -41,6 +41,8 @@ internal interface InputSizingProps {
val textWeight: Double
val textFamily: String
val textLetterSpacing: Double
+ val tabularNums: Boolean
+ val slotDigits: Boolean
val prefixSpacing: Double
val suffixSpacing: Double
val allowFontScaling: Boolean
@@ -96,6 +98,8 @@ internal fun sizingTypographyOf(p: InputSizingProps, density: Float): Typography
fontWeight = clampedInt(p.textWeight, 100, 900, 400),
fontFamily = p.textFamily.ifEmpty { null },
letterSpacing = if (p.textLetterSpacing.isFinite()) p.textLetterSpacing.toFloat() else 0f,
+ tabularNums = p.tabularNums,
+ slotDigits = p.slotDigits,
prefixSpacing = p.prefixSpacing.takeIf { it.isFinite() }?.toFloat(),
suffixSpacing = p.suffixSpacing.takeIf { it.isFinite() }?.toFloat(),
allowFontScaling = p.allowFontScaling,
@@ -328,7 +332,7 @@ internal object InputSizing {
*/
fun plainWidthPx(f: InputFonts, format: Format, body: String): Float =
f.affixRoom(format.prefix, InputRole.PREFIX) +
- f.paint(InputRole.BODY).measureText(body) + f.bodySpacing * body.codePointCount(0, body.length) +
+ f.plainWidth(body) + f.bodySpacing * body.codePointCount(0, body.length) +
f.affixRoom(format.suffix, InputRole.SUFFIX)
/** What a plain field measures: the placeholder when empty, bullets when secure, else the text and its tail. */
@@ -386,6 +390,8 @@ internal class SizingProps(s: NitroInputSizing) : InputSizingProps {
override val textWeight = s.textWeight
override val textFamily = s.textFamily
override val textLetterSpacing = s.textLetterSpacing
+ override val tabularNums = s.tabularNums
+ override val slotDigits = s.slotDigits
override val prefixSpacing = s.prefixSpacing
override val suffixSpacing = s.suffixSpacing
override val allowFontScaling = s.allowFontScaling
diff --git a/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/NitroInputView.kt b/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/NitroInputView.kt
index 573214d..9ab7dfb 100644
--- a/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/NitroInputView.kt
+++ b/packages/react-native-nitro-input/android/src/main/java/com/margelo/nitro/nitroinput/NitroInputView.kt
@@ -142,6 +142,10 @@ class NitroInputView(context: Context) : FrameLayout(context) {
val suffixAlign: AffixAlign = AffixAlign.BASELINE,
/** Dp added after every glyph, as `Text`'s letterSpacing; an affix gets it in proportion to its size. */
val letterSpacing: Float = 0f,
+ /** The font's tabular figures (`tnum`) rather than its proportional ones. */
+ val tabularNums: Boolean = true,
+ /** Every digit laid out as wide as the widest, centred ([DigitSlots]): tabular figures for a font without them. */
+ val slotDigits: Boolean = false,
/** Dp between the prefix and the text, and between the text and the suffix, in place of the letter spacing there. */
val prefixSpacing: Float? = null,
val suffixSpacing: Float? = null,
@@ -445,7 +449,11 @@ class NitroInputView(context: Context) : FrameLayout(context) {
(scrollX + width - compoundPaddingRight).toFloat(),
(scrollY + height).toFloat(),
)
- canvas.drawText(tail, x, baseline.toFloat(), ghostPaint)
+ if (typography.slotDigits) {
+ DigitSlots.draw(canvas, tail, x, baseline.toFloat(), ghostPaint)
+ } else {
+ canvas.drawText(tail, x, baseline.toFloat(), ghostPaint)
+ }
canvas.restore()
}
@@ -639,6 +647,16 @@ class NitroInputView(context: Context) : FrameLayout(context) {
editText.setSingleLine()
defaultHighlightColor = editText.highlightColor
editText.addTextChangedListener(Watcher())
+ // After it, and on every change, the engine's own writes included.
+ editText.addTextChangedListener(object : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
+
+ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
+
+ override fun afterTextChanged(s: Editable) {
+ DigitSlots.apply(s, editText.paint, off = !slotsDigits)
+ }
+ })
editText.setOnFocusChangeListener { _, focused ->
if (resyncingFocus) return@setOnFocusChangeListener
restartBlink()
@@ -770,6 +788,13 @@ class NitroInputView(context: Context) : FrameLayout(context) {
private fun applyEditTextLayout() {
val f = fonts
editText.typeface = f.body.typeface
+ // The paints' tabular figures too, which the field's own text, its hint and
+ // keepPlaceholder's tail (drawn in a copy of its paint) did not get: with a
+ // font whose digits are proportional unless asked (ABC Diatype) every typed
+ // "1" was narrower than the "0" of the placeholder it replaced, and the
+ // rest of the placeholder slid left by the difference on each keystroke.
+ // A font with no tabular figures at all is slotted (applyDigitSlots).
+ editText.fontFeatureSettings = f.body.fontFeatureSettings
// Spaced like the overlay, so the selection highlight lines up with the drawn glyphs.
editText.letterSpacing = if (f.body.textSize > 0f) f.bodySpacing / f.body.textSize else 0f
// Not `setTextSize` directly: the platform ignores it while auto-sizing is
@@ -829,6 +854,22 @@ class NitroInputView(context: Context) : FrameLayout(context) {
Alignment.CENTER -> Gravity.CENTER_HORIZONTAL
Alignment.RIGHT -> Gravity.RIGHT
}
+ applyDigitSlots()
+ }
+
+ /**
+ * Gives the edit text's digits, and its hint's, the widest digit's advance
+ * when the font has no tabular figures ([DigitSlots]), as [InputFonts] does
+ * for the overlay and the measure. A secure field's bullets stand where the
+ * digits are, so it has none.
+ */
+ /** Whether the edit text's own digits are slotted: when asked, and not under a secure field's bullets. */
+ private val slotsDigits: Boolean
+ get() = typography.slotDigits && !keyboard.secureTextEntry
+
+ private fun applyDigitSlots() {
+ editText.text?.let { DigitSlots.apply(it, editText.paint, off = !slotsDigits) }
+ applyHint(force = true)
}
// endregion
@@ -883,7 +924,10 @@ class NitroInputView(context: Context) : FrameLayout(context) {
// A password input type switches the field to monospace, so its bullets
// were wider than the field measures them; React Native's TextInput puts
// the typeface back the same way.
- if (k.secureTextEntry) editText.typeface = fonts.body.typeface
+ if (k.secureTextEntry) {
+ editText.typeface = fonts.body.typeface
+ editText.fontFeatureSettings = fonts.body.fontFeatureSettings
+ }
if (rawType != null) editText.setRawInputType(rawType)
if (k.multiline) {
editText.setSingleLine(false)
@@ -902,6 +946,7 @@ class NitroInputView(context: Context) : FrameLayout(context) {
if (k.secureTextEntry) {
editText.transformationMethod = android.text.method.PasswordTransformationMethod.getInstance()
}
+ applyDigitSlots()
val length = editText.text?.length ?: 0
if (selectionStart >= 0 && (editText.selectionStart != selectionStart || editText.selectionEnd != selectionEnd)) {
editText.setSelection(minOf(selectionStart, length), minOf(maxOf(selectionStart, selectionEnd), length))
@@ -1006,6 +1051,7 @@ class NitroInputView(context: Context) : FrameLayout(context) {
if (edge == Gravity.END) side else 0, 0)
view.gravity = Gravity.CENTER_VERTICAL
view.typeface = paint.typeface
+ view.fontFeatureSettings = paint.fontFeatureSettings
view.setTextSize(TypedValue.COMPLEX_UNIT_PX, paint.textSize)
view.letterSpacing = if (paint.textSize > 0f) fonts.spacing(role) / paint.textSize else 0f
// The seam against the text, and the affix's own offset.
@@ -1860,10 +1906,12 @@ class NitroInputView(context: Context) : FrameLayout(context) {
}
if (g.scale != 1f) canvas.scale(g.scale, g.scale, g.x + g.width / 2f, lineHeight / 2f)
val baseline = f.baseline(role, str, 0f) + g.y * lineHeight
+ // A digit in a font without tabular figures, centred in its slot.
+ val inset = f.inset(g.character, role)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
- glyphPositions = f.shaped(g.character, role).draw(canvas, g.x, baseline, paint, glyphPositions)
+ glyphPositions = f.shaped(g.character, role).draw(canvas, g.x + inset, baseline, paint, glyphPositions)
} else {
- canvas.drawText(str, g.x, baseline, paint)
+ canvas.drawText(str, g.x + inset, baseline, paint)
}
canvas.restore()
}
@@ -2053,9 +2101,12 @@ class NitroInputView(context: Context) : FrameLayout(context) {
}
/** The placeholder waits for a resting label to float clear of the line. */
- private fun applyHint() {
+ private fun applyHint(force: Boolean = false) {
val hint = effectivePlaceholder
- if (editText.hint?.toString() != hint) editText.hint = hint
+ if (!force && editText.hint?.toString() == hint) return
+ val spanned = android.text.SpannableString(hint)
+ DigitSlots.apply(spanned, editText.paint, off = !typography.slotDigits)
+ editText.hint = spanned
}
/** Re-feeds the engine so the placeholder follows the label. */
diff --git a/packages/react-native-nitro-input/ios/HybridNitroInputView.swift b/packages/react-native-nitro-input/ios/HybridNitroInputView.swift
index d1803cd..6ebb69e 100644
--- a/packages/react-native-nitro-input/ios/HybridNitroInputView.swift
+++ b/packages/react-native-nitro-input/ios/HybridNitroInputView.swift
@@ -118,6 +118,8 @@ final class HybridNitroInputView: HybridNitroInputViewSpec, RecyclableView {
var prefixAlign: NitroInputAffixAlign = .baseline { didSet { markConfigDirty() } }
var suffixAlign: NitroInputAffixAlign = .baseline { didSet { markConfigDirty() } }
var textLetterSpacing: Double = 0 { didSet { markConfigDirty() } }
+ var tabularNums: Bool = true { didSet { markConfigDirty() } }
+ var slotDigits: Bool = false { didSet { markConfigDirty() } }
var prefixSpacing: Double = .infinity { didSet { markConfigDirty() } }
var suffixSpacing: Double = .infinity { didSet { markConfigDirty() } }
var prefixOffset: Double = 0 { didSet { markConfigDirty() } }
@@ -337,6 +339,8 @@ final class HybridNitroInputView: HybridNitroInputViewSpec, RecyclableView {
prefixAlign = .baseline
suffixAlign = .baseline
textLetterSpacing = 0
+ tabularNums = true
+ slotDigits = false
prefixSpacing = .infinity
suffixSpacing = .infinity
prefixOffset = 0
diff --git a/packages/react-native-nitro-input/ios/InputSizing.swift b/packages/react-native-nitro-input/ios/InputSizing.swift
index f195b40..a83315a 100644
--- a/packages/react-native-nitro-input/ios/InputSizing.swift
+++ b/packages/react-native-nitro-input/ios/InputSizing.swift
@@ -47,6 +47,8 @@ protocol InputSizingProps {
var textWeight: Double { get }
var textFamily: String { get }
var textLetterSpacing: Double { get }
+ var tabularNums: Bool { get }
+ var slotDigits: Bool { get }
var prefixSpacing: Double { get }
var suffixSpacing: Double { get }
var allowFontScaling: Bool { get }
@@ -108,6 +110,8 @@ enum InputMapping {
typography.fontWeight = CGFloat(p.textWeight.isFinite ? p.textWeight : 400)
typography.fontFamily = p.textFamily.isEmpty ? nil : p.textFamily
typography.letterSpacing = CGFloat(p.textLetterSpacing.isFinite ? p.textLetterSpacing : 0)
+ typography.tabularNums = p.tabularNums
+ typography.slotDigits = p.slotDigits
typography.prefixSpacing = p.prefixSpacing.isFinite ? CGFloat(p.prefixSpacing) : nil
typography.suffixSpacing = p.suffixSpacing.isFinite ? CGFloat(p.suffixSpacing) : nil
typography.allowFontScaling = p.allowFontScaling
@@ -215,6 +219,17 @@ final class InputMetrics {
let lineBoxHeight: CGFloat
/// The advance of one of the bullets a plain secure field draws (see `init`).
let secureBulletWidth: CGFloat
+ /// The body's digit advances when they are slotted (`tabularNums` set) and
+ /// the font has no tabular figures; nil otherwise. The monospaced-numbers feature asks for them, but a font
+ /// without it (Open Runde) keeps proportional digits: every typed "1" was
+ /// narrower than the placeholder's "0" it replaced, so `keepPlaceholder`'s
+ /// tail slid left on each keystroke and an amount changed width as its
+ /// digits did. Such a font's digits each get a slot as wide as the widest,
+ /// centred in it, as NitroNumber's columns do (and as Android's DigitSlots
+ /// does). The affixes keep their own: they never change under the typing.
+ let bodyDigitWidths: [CGFloat]?
+ /// The slot a body digit is laid out in, when `bodyDigitWidths` is set.
+ let bodySlot: CGFloat
private var widthCache: [WidthKey: CGFloat] = [:]
private struct WidthKey: Hashable {
@@ -224,9 +239,11 @@ final class InputMetrics {
init(_ t: NitroInputView.Typography) {
let scale = NitroInputView.systemFontMultiplier(t)
- body = NitroInputView.makeFont(size: t.fontSize * scale, weight: t.fontWeight, family: t.fontFamily)
- prefix = NitroInputView.makeFont(size: (t.prefixFontSize ?? t.fontSize) * scale, weight: t.fontWeight, family: t.fontFamily)
- suffix = NitroInputView.makeFont(size: (t.suffixFontSize ?? t.fontSize) * scale, weight: t.fontWeight, family: t.fontFamily)
+ body = NitroInputView.makeFont(size: t.fontSize * scale, weight: t.fontWeight, family: t.fontFamily, tabular: t.tabularNums)
+ prefix = NitroInputView.makeFont(size: (t.prefixFontSize ?? t.fontSize) * scale, weight: t.fontWeight, family: t.fontFamily,
+ tabular: t.tabularNums)
+ suffix = NitroInputView.makeFont(size: (t.suffixFontSize ?? t.fontSize) * scale, weight: t.fontWeight, family: t.fontFamily,
+ tabular: t.tabularNums)
bodySpacing = t.letterSpacing * scale
prefixLetterSpacing = t.letterSpacing * scale * (prefix.pointSize / body.pointSize)
suffixLetterSpacing = t.letterSpacing * scale * (suffix.pointSize / body.pointSize)
@@ -245,6 +262,10 @@ final class InputMetrics {
? UIFont.systemFont(ofSize: body.pointSize, weight: NitroInputView.uiWeight(t.fontWeight))
: UIFont.systemFont(ofSize: body.pointSize)
secureBulletWidth = ("\u{2219}" as NSString).size(withAttributes: [.font: bulletFont]).width
+ let bodyFont = body
+ let digits = (0..<10).map { NSAttributedString(string: String($0), attributes: [.font: bodyFont, .kern: 0]).size().width }
+ bodySlot = digits.max() ?? 0
+ bodyDigitWidths = t.slotDigits && bodySlot - (digits.min() ?? 0) > bodyFont.pointSize * 0.01 ? digits : nil
}
func font(for role: NitroInputView.GlyphRole) -> UIFont {
@@ -268,11 +289,84 @@ final class InputMetrics {
func width(of text: String, role: NitroInputView.GlyphRole) -> CGFloat {
let key = WidthKey(text: text, role: role)
if let cached = widthCache[key] { return cached }
- let width = NSAttributedString(string: text, attributes: [.font: font(for: role), .kern: 0]).size().width
+ var width = NSAttributedString(string: text, attributes: [.font: font(for: role), .kern: 0]).size().width
+ if role == .body, bodyDigitWidths != nil {
+ for scalar in text.unicodeScalars { width += 2 * halfRoom(scalar) }
+ }
widthCache[key] = width
return width
}
+ private static func digit(_ scalar: Unicode.Scalar) -> Int? {
+ scalar.value >= 0x30 && scalar.value <= 0x39 ? Int(scalar.value - 0x30) : nil
+ }
+
+ /// Half the room a body digit's slot leaves beside its glyph (0 for anything else).
+ private func halfRoom(_ scalar: Unicode.Scalar) -> CGFloat {
+ guard let widths = bodyDigitWidths, let digit = Self.digit(scalar) else { return 0 }
+ return (bodySlot - widths[digit]) / 2
+ }
+
+ private var pairKernCache: [UInt64: CGFloat] = [:]
+
+ /// The body font's own kerning between `a` and `b`, which non-zero `.kern`s on both leave in place.
+ private func pairKern(_ a: Unicode.Scalar, _ b: Unicode.Scalar) -> CGFloat {
+ let key = UInt64(a.value) << 32 | UInt64(b.value)
+ if let cached = pairKernCache[key] { return cached }
+ // The pair with a kern of 1 on both glyphs, against the same with kerning off.
+ func line(_ kern: CGFloat) -> CGFloat {
+ let pair = NSAttributedString(string: String(a) + String(b), attributes: [.font: body, .kern: kern])
+ return CGFloat(CTLineGetTypographicBounds(CTLineCreateWithAttributedString(pair), nil, nil, nil))
+ }
+ let kern = line(1) - 2 - line(0)
+ pairKernCache[key] = kern
+ return kern
+ }
+
+ /// How far in from the left of its advance to draw `text` (one glyph): a slotted digit is centred in it.
+ func inset(of text: String, role: NitroInputView.GlyphRole) -> CGFloat {
+ guard role == .body, let scalar = text.unicodeScalars.first else { return 0 }
+ return halfRoom(scalar)
+ }
+
+ /// `text` in `attributes`, kerned for UIKit to lay out as the overlay and the
+ /// measure do: the body spacing after every glyph and each digit in its slot.
+ /// Kerning only adds room after a glyph, so half a digit's room goes after
+ /// the glyph before it and half after its own. A first digit has none before
+ /// it: with `lead`, that half is returned for the caller to move the text by
+ /// (the tail's label); without, the first line is indented by it (the
+ /// field's text and placeholder, which UIKit places itself). Nil when the
+ /// digits are not slotted.
+ func slotted(_ text: String, attributes: [NSAttributedString.Key: Any], lead: Bool) -> (string: NSAttributedString, lead: CGFloat)? {
+ guard bodyDigitWidths != nil else { return nil }
+ let result = NSMutableAttributedString(string: text, attributes: attributes)
+ let scalars = Array(text.unicodeScalars)
+ let halves = scalars.map(halfRoom)
+ var leading: CGFloat = 0
+ var offset = 0
+ let kerns = halves.indices.map { bodySpacing + halves[$0] + ($0 + 1 < halves.count ? halves[$0 + 1] : 0) }
+ for (i, scalar) in scalars.enumerated() {
+ var kern = kerns[i]
+ // The font's kerning between two glyphs comes back once both carry a
+ // kern other than 0 (Open Runde kerns "74" by -0.058 em). The measure
+ // has it off, so it is taken back out of the first one's.
+ if i + 1 < scalars.count, kern != 0, kerns[i + 1] != 0 {
+ kern -= pairKern(scalar, scalars[i + 1])
+ if kern == 0 { kern = 0.0001 }
+ }
+ if i == 0 { leading = halves[0] }
+ let length = String(scalar).utf16.count
+ result.addAttribute(.kern, value: kern, range: NSRange(location: offset, length: length))
+ offset += length
+ }
+ if !lead, leading > 0 {
+ let style = ((attributes[.paragraphStyle] as? NSParagraphStyle)?.mutableCopy() as? NSMutableParagraphStyle) ?? NSMutableParagraphStyle()
+ style.firstLineHeadIndent = leading
+ result.addAttribute(.paragraphStyle, value: style, range: NSRange(location: 0, length: result.length))
+ }
+ return (result, leading)
+ }
+
/// The room an affix takes beside the text: its glyphs, their spacing and the seam.
func affixRoom(_ affix: String, role: NitroInputView.GlyphRole) -> CGFloat {
guard !affix.isEmpty else { return 0 }
diff --git a/packages/react-native-nitro-input/ios/NitroInputView.swift b/packages/react-native-nitro-input/ios/NitroInputView.swift
index af01810..8c268f4 100644
--- a/packages/react-native-nitro-input/ios/NitroInputView.swift
+++ b/packages/react-native-nitro-input/ios/NitroInputView.swift
@@ -144,6 +144,10 @@ final class NitroInputView: UIView {
var suffixAlign: AffixAlign = .baseline
/// Points added after every glyph, as `Text`'s letterSpacing; an affix gets it in proportion to its size.
var letterSpacing: CGFloat = 0
+ /// The font's tabular figures rather than its proportional ones.
+ var tabularNums = true
+ /// Every digit laid out as wide as the widest, centred (`InputMetrics.bodyDigitWidths`): tabular figures for a font without them.
+ var slotDigits = false
/// Points between the prefix and the text, and between the text and the suffix, in place of the letter spacing there.
var prefixSpacing: CGFloat? = nil
var suffixSpacing: CGFloat? = nil
@@ -293,6 +297,9 @@ final class NitroInputView: UIView {
didSet {
guard alignment != oldValue else { return }
field.textAlignment = textAlignment(for: alignment)
+ // The new alignment is the field's paragraph style, which the slots' indent rides on.
+ applyDigitSlots()
+ syncAccessibilityPlaceholder()
render()
}
}
@@ -677,6 +684,8 @@ final class NitroInputView: UIView {
private struct GlyphEntry {
let layer: CALayer
let topOffset: CGFloat
+ /// A slotted digit's offset into its advance (`InputMetrics.inset`).
+ let leftInset: CGFloat
/// The render pass that last saw this glyph; one a pass missed has left.
var generation: UInt
/// What the layer was last given, so a frame only sets what moved: most
@@ -791,12 +800,18 @@ final class NitroInputView: UIView {
// field's own copy exists only so an empty field still has an
// accessibility value, hence clear.
let color = traits.plain ? typography.placeholderColor : UIColor.clear
- let unchanged = field.attributedPlaceholder?.string == placeholder
- && field.attributedPlaceholder?.attribute(.foregroundColor, at: 0, effectiveRange: nil) as? UIColor == color
- guard !unchanged || placeholder.isEmpty != (field.attributedPlaceholder == nil) else { return }
- field.attributedPlaceholder = placeholder.isEmpty
+ // Its digits slotted like the text's, or the placeholder is narrower than
+ // the field measures it, and a typed digit moves the rest along. In the
+ // field's own attributes, which carry its alignment.
+ var slotAttributes = field.defaultTextAttributes
+ slotAttributes[.foregroundColor] = color
+ let next = placeholder.isEmpty
? nil
- : NSAttributedString(string: placeholder, attributes: [.foregroundColor: color])
+ : fonts.metrics.slotted(placeholder, attributes: slotAttributes, lead: false)?.string
+ ?? NSAttributedString(string: placeholder, attributes: [.foregroundColor: color])
+ if let current = field.attributedPlaceholder, let next, current.isEqual(to: next) { return }
+ if field.attributedPlaceholder == nil, next == nil { return }
+ field.attributedPlaceholder = next
}
override func didMoveToSuperview() {
@@ -844,6 +859,8 @@ final class NitroInputView: UIView {
private func applyLayoutDirection() {
engine.setRightToLeft(isRTL)
field.textAlignment = traits.plain ? textAlignment(for: alignment) : .left
+ applyDigitSlots()
+ syncAccessibilityPlaceholder()
if didBuildTextView {
textView.textAlignment = textAlignment(for: alignment)
applyTextViewTypography()
@@ -1313,10 +1330,49 @@ final class NitroInputView: UIView {
attributes[.font] = fonts.body
attributes[.foregroundColor] = traits.plain ? typography.color : UIColor.clear
field.defaultTextAttributes = attributes
+ fieldSlotted = false
+ applyDigitSlots()
+ syncAccessibilityPlaceholder()
updateFieldInsets()
applyTextViewTypography()
}
+ /// Whether the field's text carries `applyDigitSlots`' kerning.
+ private var fieldSlotted = false
+
+ /// Lays the field's own digits out in the slots the overlay and the measure
+ /// give them when the font has no tabular figures (`InputMetrics.slotted`):
+ /// plain mode draws that text, and otherwise the caret and the selection
+ /// follow it. Setting `attributedText` moves the selection, so it is put
+ /// back; a composition in progress is left alone until it is committed.
+ private func applyDigitSlots() {
+ guard field.markedTextRange == nil else { return }
+ let current = field.text ?? ""
+ guard !traits.secureTextEntry, !current.isEmpty,
+ let slotted = fonts.metrics.slotted(current, attributes: field.defaultTextAttributes, lead: false)?.string else {
+ if fieldSlotted {
+ // Back to one kerning for the whole text: re-setting the attributes applies them throughout.
+ fieldSlotted = false
+ let attributes = field.defaultTextAttributes
+ field.defaultTextAttributes = attributes
+ }
+ return
+ }
+ if let existing = field.attributedText, existing.isEqual(to: slotted) { return }
+ let start = field.beginningOfDocument
+ let selection = field.selectedTextRange.map { (field.offset(from: start, to: $0.start), field.offset(from: start, to: $0.end)) }
+ let wasSetting = isSettingText
+ isSettingText = true
+ field.attributedText = slotted
+ if let (from, to) = selection,
+ let fromPosition = field.position(from: field.beginningOfDocument, offset: from),
+ let toPosition = field.position(from: field.beginningOfDocument, offset: to) {
+ field.selectedTextRange = field.textRange(from: fromPosition, to: toPosition)
+ }
+ isSettingText = wasSetting
+ fieldSlotted = true
+ }
+
/// In reflow mode the overlay draws the prefix and suffix, so the field only
/// has to reserve the space. In `plain` mode nothing is drawing them, so the
/// field carries them itself as its left/right accessory views — UIKit insets
@@ -1582,6 +1638,8 @@ final class NitroInputView: UIView {
var attributes = field.defaultTextAttributes
attributes[.foregroundColor] = plain ? typography.color : UIColor.clear
field.defaultTextAttributes = attributes
+ fieldSlotted = false
+ applyDigitSlots()
updateFieldInsets()
// Turning plain on tore the overlay down and reset the engine, so on the
// way back it has nothing to draw until the next edit: hand it the text.
@@ -1678,6 +1736,7 @@ final class NitroInputView: UIView {
/// `changed` is false only for the first, unchanged application of the
/// `text` prop (the engine still needs feeding); a keystroke always changes.
private func textDidChange(caret: Int, reason: ChangeReason, changed: Bool = true) {
+ applyDigitSlots()
feedEngine(caret: caret)
if reason != .prop {
eventCount += 1
@@ -1897,7 +1956,7 @@ final class NitroInputView: UIView {
return multiplier
}
- static func makeFont(size: CGFloat, weight: CGFloat, family: String?) -> UIFont {
+ static func makeFont(size: CGFloat, weight: CGFloat, family: String?, tabular: Bool = true) -> UIFont {
let uiWeight = uiWeight(weight)
let traits: [UIFontDescriptor.TraitKey: Any] = [.weight: uiWeight]
var base: UIFont
@@ -1910,8 +1969,11 @@ final class NitroInputView: UIView {
} else {
base = UIFont.systemFont(ofSize: size, weight: uiWeight)
}
- // Tabular digits: a column keeps its width while its digit reflows.
- let feature: [UIFontDescriptor.FeatureKey: Int] = [.type: kNumberSpacingType, .selector: kMonospacedNumbersSelector]
+ // Tabular digits, so a column keeps its width while its digit reflows, unless asked for the font's own.
+ let feature: [UIFontDescriptor.FeatureKey: Int] = [
+ .type: kNumberSpacingType,
+ .selector: tabular ? kMonospacedNumbersSelector : kProportionalNumbersSelector,
+ ]
let descriptor = base.fontDescriptor.addingAttributes([.featureSettings: [feature]])
return UIFont(descriptor: descriptor, size: size)
}
@@ -1999,7 +2061,10 @@ final class NitroInputView: UIView {
if ghostLabel.superview !== field { field.addSubview(ghostLabel) }
var attributes = field.defaultTextAttributes
attributes[.foregroundColor] = typography.placeholderColor.resolvedColor(with: traitCollection)
- ghostLabel.attributedText = NSAttributedString(string: tail, attributes: attributes)
+ // A first slotted digit's half-room before it moves the label instead (`InputMetrics.slotted`).
+ let slotted = fonts.metrics.slotted(tail, attributes: attributes, lead: true)
+ ghostLabel.attributedText = slotted?.string ?? NSAttributedString(string: tail, attributes: attributes)
+ let lead = slotted?.lead ?? 0
let box = field.textRect(forBounds: field.bounds)
let typed = fonts.width(of: text, role: .body) + fonts.bodySpacing * CGFloat(text.unicodeScalars.count)
let tailWidth = fonts.width(of: tail, role: .body) + fonts.bodySpacing * CGFloat(tail.unicodeScalars.count)
@@ -2014,7 +2079,8 @@ final class NitroInputView: UIView {
let end = start + typed
// Text wider than the box scrolls inside the field; the tail has no place then.
ghostLabel.isHidden = typed > box.width
- ghostLabel.frame = CGRect(x: end, y: box.minY, width: min(tailWidth + 1, max(0, box.maxX - end)), height: box.height)
+ ghostLabel.frame = CGRect(x: end + lead, y: box.minY, width: min(tailWidth - lead + 1, max(0, box.maxX - end - lead)),
+ height: box.height)
}
private func contentWidth() -> CGFloat {
@@ -2177,14 +2243,15 @@ final class NitroInputView: UIView {
glyphLayer.bounds = CGRect(origin: .zero, size: image.size)
}
clipLayer.addSublayer(glyphLayer)
- glyphLayers[g.id] = GlyphEntry(layer: glyphLayer, topOffset: fonts.top(for: role, text: text, lineTop: 0), generation: generation)
+ glyphLayers[g.id] = GlyphEntry(layer: glyphLayer, topOffset: fonts.top(for: role, text: text, lineTop: 0),
+ leftInset: fonts.metrics.inset(of: text, role: role), generation: generation)
index = glyphLayers.index(forKey: g.id)!
}
let entry = glyphLayers.values[index]
let glyphLayer = entry.layer
let size = glyphLayer.bounds.size
let top = band + entry.topOffset + CGFloat(g.y) * lineHeight
- let position = CGPoint(x: pad + CGFloat(g.x) + size.width / 2, y: top + size.height / 2)
+ let position = CGPoint(x: pad + CGFloat(g.x) + entry.leftInset + size.width / 2, y: top + size.height / 2)
if position != entry.position {
glyphLayer.position = position
glyphLayers.values[index].position = position
diff --git a/packages/react-native-nitro-input/nitrogen/generated/android/c++/JHybridNitroInputViewSpec.cpp b/packages/react-native-nitro-input/nitrogen/generated/android/c++/JHybridNitroInputViewSpec.cpp
index ad10c89..25182bb 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/android/c++/JHybridNitroInputViewSpec.cpp
+++ b/packages/react-native-nitro-input/nitrogen/generated/android/c++/JHybridNitroInputViewSpec.cpp
@@ -509,6 +509,24 @@ namespace margelo::nitro::nitroinput {
static const auto method = _javaPart->javaClassStatic()->getMethod("setTextLetterSpacing");
method(_javaPart, textLetterSpacing);
}
+ bool JHybridNitroInputViewSpec::getTabularNums() {
+ static const auto method = _javaPart->javaClassStatic()->getMethod("getTabularNums");
+ auto __result = method(_javaPart);
+ return static_cast(__result);
+ }
+ void JHybridNitroInputViewSpec::setTabularNums(bool tabularNums) {
+ static const auto method = _javaPart->javaClassStatic()->getMethod("setTabularNums");
+ method(_javaPart, tabularNums);
+ }
+ bool JHybridNitroInputViewSpec::getSlotDigits() {
+ static const auto method = _javaPart->javaClassStatic()->getMethod("getSlotDigits");
+ auto __result = method(_javaPart);
+ return static_cast(__result);
+ }
+ void JHybridNitroInputViewSpec::setSlotDigits(bool slotDigits) {
+ static const auto method = _javaPart->javaClassStatic()->getMethod("setSlotDigits");
+ method(_javaPart, slotDigits);
+ }
double JHybridNitroInputViewSpec::getPrefixSpacing() {
static const auto method = _javaPart->javaClassStatic()->getMethod("getPrefixSpacing");
auto __result = method(_javaPart);
diff --git a/packages/react-native-nitro-input/nitrogen/generated/android/c++/JHybridNitroInputViewSpec.hpp b/packages/react-native-nitro-input/nitrogen/generated/android/c++/JHybridNitroInputViewSpec.hpp
index d6cf3ea..2049142 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/android/c++/JHybridNitroInputViewSpec.hpp
+++ b/packages/react-native-nitro-input/nitrogen/generated/android/c++/JHybridNitroInputViewSpec.hpp
@@ -124,6 +124,10 @@ namespace margelo::nitro::nitroinput {
void setSuffixAlign(NitroInputAffixAlign suffixAlign) override;
double getTextLetterSpacing() override;
void setTextLetterSpacing(double textLetterSpacing) override;
+ bool getTabularNums() override;
+ void setTabularNums(bool tabularNums) override;
+ bool getSlotDigits() override;
+ void setSlotDigits(bool slotDigits) override;
double getPrefixSpacing() override;
void setPrefixSpacing(double prefixSpacing) override;
double getSuffixSpacing() override;
diff --git a/packages/react-native-nitro-input/nitrogen/generated/android/c++/JNitroInputSizing.hpp b/packages/react-native-nitro-input/nitrogen/generated/android/c++/JNitroInputSizing.hpp
index e5f1b82..666fea3 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/android/c++/JNitroInputSizing.hpp
+++ b/packages/react-native-nitro-input/nitrogen/generated/android/c++/JNitroInputSizing.hpp
@@ -110,6 +110,10 @@ namespace margelo::nitro::nitroinput {
jni::local_ref textFamily = this->getFieldValue(fieldTextFamily);
static const auto fieldTextLetterSpacing = clazz->getField("textLetterSpacing");
double textLetterSpacing = this->getFieldValue(fieldTextLetterSpacing);
+ static const auto fieldTabularNums = clazz->getField("tabularNums");
+ jboolean tabularNums = this->getFieldValue(fieldTabularNums);
+ static const auto fieldSlotDigits = clazz->getField("slotDigits");
+ jboolean slotDigits = this->getFieldValue(fieldSlotDigits);
static const auto fieldPrefixSpacing = clazz->getField("prefixSpacing");
double prefixSpacing = this->getFieldValue(fieldPrefixSpacing);
static const auto fieldSuffixSpacing = clazz->getField("suffixSpacing");
@@ -187,6 +191,8 @@ namespace margelo::nitro::nitroinput {
textWeight,
textFamily->toStdString(),
textLetterSpacing,
+ static_cast(tabularNums),
+ static_cast(slotDigits),
prefixSpacing,
suffixSpacing,
static_cast(allowFontScaling),
@@ -205,7 +211,7 @@ namespace margelo::nitro::nitroinput {
*/
[[maybe_unused]]
static jni::local_ref fromCpp(const NitroInputSizing& value) {
- using JSignature = JNitroInputSizing(jni::alias_ref, jni::alias_ref, jboolean, jboolean, jboolean, double, double, double, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref>, jboolean, jboolean, jni::alias_ref>, jni::alias_ref, jni::alias_ref, jni::alias_ref>, jni::alias_ref, jboolean, double, double, double, double, double, jni::alias_ref, double, double, double, jboolean, double, jni::alias_ref, jni::alias_ref, jni::alias_ref, double, double);
+ using JSignature = JNitroInputSizing(jni::alias_ref, jni::alias_ref, jboolean, jboolean, jboolean, double, double, double, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref, jni::alias_ref>, jboolean, jboolean, jni::alias_ref>, jni::alias_ref, jni::alias_ref, jni::alias_ref>, jni::alias_ref, jboolean, double, double, double, double, double, jni::alias_ref, double, jboolean, jboolean, double, double, jboolean, double, jni::alias_ref, jni::alias_ref, jni::alias_ref, double, double);
static const auto clazz = javaClassStatic();
static const auto create = clazz->getStaticMethod("fromCpp");
return create(
@@ -268,6 +274,8 @@ namespace margelo::nitro::nitroinput {
value.textWeight,
jni::make_jstring(value.textFamily),
value.textLetterSpacing,
+ value.tabularNums,
+ value.slotDigits,
value.prefixSpacing,
value.suffixSpacing,
value.allowFontScaling,
diff --git a/packages/react-native-nitro-input/nitrogen/generated/android/c++/views/JHybridNitroInputViewStateUpdater.cpp b/packages/react-native-nitro-input/nitrogen/generated/android/c++/views/JHybridNitroInputViewStateUpdater.cpp
index c963ef6..69f51ef 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/android/c++/views/JHybridNitroInputViewStateUpdater.cpp
+++ b/packages/react-native-nitro-input/nitrogen/generated/android/c++/views/JHybridNitroInputViewStateUpdater.cpp
@@ -238,6 +238,16 @@ void JHybridNitroInputViewStateUpdater::updateViewProps(jni::alias_reftextLetterSpacing.hasSameValue(oldProps->textLetterSpacing)) {
hybridView->setTextLetterSpacing(newProps->textLetterSpacing.get());
}
+ if (oldProps == nullptr
+ ? newProps->tabularNums.isProvided()
+ : !newProps->tabularNums.hasSameValue(oldProps->tabularNums)) {
+ hybridView->setTabularNums(newProps->tabularNums.get());
+ }
+ if (oldProps == nullptr
+ ? newProps->slotDigits.isProvided()
+ : !newProps->slotDigits.hasSameValue(oldProps->slotDigits)) {
+ hybridView->setSlotDigits(newProps->slotDigits.get());
+ }
if (oldProps == nullptr
? newProps->prefixSpacing.isProvided()
: !newProps->prefixSpacing.hasSameValue(oldProps->prefixSpacing)) {
diff --git a/packages/react-native-nitro-input/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinput/HybridNitroInputViewSpec.kt b/packages/react-native-nitro-input/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinput/HybridNitroInputViewSpec.kt
index da3d0c3..b04d71b 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinput/HybridNitroInputViewSpec.kt
+++ b/packages/react-native-nitro-input/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinput/HybridNitroInputViewSpec.kt
@@ -249,6 +249,18 @@ abstract class HybridNitroInputViewSpec: HybridView() {
@set:Keep
abstract var textLetterSpacing: Double
+ @get:DoNotStrip
+ @get:Keep
+ @set:DoNotStrip
+ @set:Keep
+ abstract var tabularNums: Boolean
+
+ @get:DoNotStrip
+ @get:Keep
+ @set:DoNotStrip
+ @set:Keep
+ abstract var slotDigits: Boolean
+
@get:DoNotStrip
@get:Keep
@set:DoNotStrip
diff --git a/packages/react-native-nitro-input/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinput/NitroInputSizing.kt b/packages/react-native-nitro-input/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinput/NitroInputSizing.kt
index 1cfe3d1..1fe7c0d 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinput/NitroInputSizing.kt
+++ b/packages/react-native-nitro-input/nitrogen/generated/android/kotlin/com/margelo/nitro/nitroinput/NitroInputSizing.kt
@@ -113,6 +113,12 @@ data class NitroInputSizing(
val textLetterSpacing: Double,
@DoNotStrip
@Keep
+ val tabularNums: Boolean,
+ @DoNotStrip
+ @Keep
+ val slotDigits: Boolean,
+ @DoNotStrip
+ @Keep
val prefixSpacing: Double,
@DoNotStrip
@Keep
@@ -175,6 +181,8 @@ data class NitroInputSizing(
&& Objects.deepEquals(this.textWeight, other.textWeight)
&& Objects.deepEquals(this.textFamily, other.textFamily)
&& Objects.deepEquals(this.textLetterSpacing, other.textLetterSpacing)
+ && Objects.deepEquals(this.tabularNums, other.tabularNums)
+ && Objects.deepEquals(this.slotDigits, other.slotDigits)
&& Objects.deepEquals(this.prefixSpacing, other.prefixSpacing)
&& Objects.deepEquals(this.suffixSpacing, other.suffixSpacing)
&& Objects.deepEquals(this.allowFontScaling, other.allowFontScaling)
@@ -219,6 +227,8 @@ data class NitroInputSizing(
textWeight,
textFamily,
textLetterSpacing,
+ tabularNums,
+ slotDigits,
prefixSpacing,
suffixSpacing,
allowFontScaling,
@@ -239,8 +249,8 @@ data class NitroInputSizing(
@Keep
@Suppress("unused")
@JvmStatic
- private fun fromCpp(text: String, mode: NitroInputMode, plain: Boolean, multiline: Boolean, secureTextEntry: Boolean, maxLength: Double, fractionDigits: Double, maxIntegerDigits: Double, groupingSeparator: String, decimalSeparator: String, prefix: String, suffix: String, signPlacement: NitroInputSignPlacement, placeholder: String, mask: String, maskNotations: Array, maskAutocomplete: Boolean, maskAutoSkip: Boolean, maskAffinityFormats: Array, maskAffinityStrategy: NitroInputMaskAffinity, maskTextCase: NitroInputTextCase, maskCharacterMap: Array, maskSlotPlaceholder: String, keepPlaceholder: Boolean, textSize: Double, textLineHeight: Double, prefixFontSize: Double, suffixFontSize: Double, textWeight: Double, textFamily: String, textLetterSpacing: Double, prefixSpacing: Double, suffixSpacing: Double, allowFontScaling: Boolean, maxFontSizeMultiplier: Double, variant: NitroInputVariant, label: String, labelBehavior: NitroInputLabelBehavior, labelFontSize: Double, cornerRadius: Double): NitroInputSizing {
- return NitroInputSizing(text, mode, plain, multiline, secureTextEntry, maxLength, fractionDigits, maxIntegerDigits, groupingSeparator, decimalSeparator, prefix, suffix, signPlacement, placeholder, mask, maskNotations, maskAutocomplete, maskAutoSkip, maskAffinityFormats, maskAffinityStrategy, maskTextCase, maskCharacterMap, maskSlotPlaceholder, keepPlaceholder, textSize, textLineHeight, prefixFontSize, suffixFontSize, textWeight, textFamily, textLetterSpacing, prefixSpacing, suffixSpacing, allowFontScaling, maxFontSizeMultiplier, variant, label, labelBehavior, labelFontSize, cornerRadius)
+ private fun fromCpp(text: String, mode: NitroInputMode, plain: Boolean, multiline: Boolean, secureTextEntry: Boolean, maxLength: Double, fractionDigits: Double, maxIntegerDigits: Double, groupingSeparator: String, decimalSeparator: String, prefix: String, suffix: String, signPlacement: NitroInputSignPlacement, placeholder: String, mask: String, maskNotations: Array, maskAutocomplete: Boolean, maskAutoSkip: Boolean, maskAffinityFormats: Array, maskAffinityStrategy: NitroInputMaskAffinity, maskTextCase: NitroInputTextCase, maskCharacterMap: Array, maskSlotPlaceholder: String, keepPlaceholder: Boolean, textSize: Double, textLineHeight: Double, prefixFontSize: Double, suffixFontSize: Double, textWeight: Double, textFamily: String, textLetterSpacing: Double, tabularNums: Boolean, slotDigits: Boolean, prefixSpacing: Double, suffixSpacing: Double, allowFontScaling: Boolean, maxFontSizeMultiplier: Double, variant: NitroInputVariant, label: String, labelBehavior: NitroInputLabelBehavior, labelFontSize: Double, cornerRadius: Double): NitroInputSizing {
+ return NitroInputSizing(text, mode, plain, multiline, secureTextEntry, maxLength, fractionDigits, maxIntegerDigits, groupingSeparator, decimalSeparator, prefix, suffix, signPlacement, placeholder, mask, maskNotations, maskAutocomplete, maskAutoSkip, maskAffinityFormats, maskAffinityStrategy, maskTextCase, maskCharacterMap, maskSlotPlaceholder, keepPlaceholder, textSize, textLineHeight, prefixFontSize, suffixFontSize, textWeight, textFamily, textLetterSpacing, tabularNums, slotDigits, prefixSpacing, suffixSpacing, allowFontScaling, maxFontSizeMultiplier, variant, label, labelBehavior, labelFontSize, cornerRadius)
}
}
}
diff --git a/packages/react-native-nitro-input/nitrogen/generated/ios/c++/HybridNitroInputViewSpecSwift.hpp b/packages/react-native-nitro-input/nitrogen/generated/ios/c++/HybridNitroInputViewSpecSwift.hpp
index 09a8516..1a8581b 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/ios/c++/HybridNitroInputViewSpecSwift.hpp
+++ b/packages/react-native-nitro-input/nitrogen/generated/ios/c++/HybridNitroInputViewSpecSwift.hpp
@@ -359,6 +359,18 @@ namespace margelo::nitro::nitroinput {
inline void setTextLetterSpacing(double textLetterSpacing) noexcept override {
_swiftPart.setTextLetterSpacing(std::forward(textLetterSpacing));
}
+ inline bool getTabularNums() noexcept override {
+ return _swiftPart.getTabularNums();
+ }
+ inline void setTabularNums(bool tabularNums) noexcept override {
+ _swiftPart.setTabularNums(std::forward(tabularNums));
+ }
+ inline bool getSlotDigits() noexcept override {
+ return _swiftPart.getSlotDigits();
+ }
+ inline void setSlotDigits(bool slotDigits) noexcept override {
+ _swiftPart.setSlotDigits(std::forward(slotDigits));
+ }
inline double getPrefixSpacing() noexcept override {
return _swiftPart.getPrefixSpacing();
}
diff --git a/packages/react-native-nitro-input/nitrogen/generated/ios/c++/views/HybridNitroInputViewComponent.mm b/packages/react-native-nitro-input/nitrogen/generated/ios/c++/views/HybridNitroInputViewComponent.mm
index 7b3992f..733c861 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/ios/c++/views/HybridNitroInputViewComponent.mm
+++ b/packages/react-native-nitro-input/nitrogen/generated/ios/c++/views/HybridNitroInputViewComponent.mm
@@ -320,6 +320,18 @@ - (void) updateProps:(const std::shared_ptr&)props
: !newViewProps.textLetterSpacing.hasSameValue(oldViewProps->textLetterSpacing)) {
swiftPart.setTextLetterSpacing(newViewProps.textLetterSpacing.get());
}
+ // tabularNums: boolean
+ if (oldViewProps == nullptr
+ ? newViewProps.tabularNums.isProvided()
+ : !newViewProps.tabularNums.hasSameValue(oldViewProps->tabularNums)) {
+ swiftPart.setTabularNums(newViewProps.tabularNums.get());
+ }
+ // slotDigits: boolean
+ if (oldViewProps == nullptr
+ ? newViewProps.slotDigits.isProvided()
+ : !newViewProps.slotDigits.hasSameValue(oldViewProps->slotDigits)) {
+ swiftPart.setSlotDigits(newViewProps.slotDigits.get());
+ }
// prefixSpacing: number
if (oldViewProps == nullptr
? newViewProps.prefixSpacing.isProvided()
diff --git a/packages/react-native-nitro-input/nitrogen/generated/ios/swift/HybridNitroInputViewSpec.swift b/packages/react-native-nitro-input/nitrogen/generated/ios/swift/HybridNitroInputViewSpec.swift
index e61f408..8184c37 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/ios/swift/HybridNitroInputViewSpec.swift
+++ b/packages/react-native-nitro-input/nitrogen/generated/ios/swift/HybridNitroInputViewSpec.swift
@@ -47,6 +47,8 @@ public protocol HybridNitroInputViewSpec_protocol: HybridObject, HybridView {
var prefixAlign: NitroInputAffixAlign { get set }
var suffixAlign: NitroInputAffixAlign { get set }
var textLetterSpacing: Double { get set }
+ var tabularNums: Bool { get set }
+ var slotDigits: Bool { get set }
var prefixSpacing: Double { get set }
var suffixSpacing: Double { get set }
var prefixOffset: Double { get set }
diff --git a/packages/react-native-nitro-input/nitrogen/generated/ios/swift/HybridNitroInputViewSpec_cxx.swift b/packages/react-native-nitro-input/nitrogen/generated/ios/swift/HybridNitroInputViewSpec_cxx.swift
index c133281..c041797 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/ios/swift/HybridNitroInputViewSpec_cxx.swift
+++ b/packages/react-native-nitro-input/nitrogen/generated/ios/swift/HybridNitroInputViewSpec_cxx.swift
@@ -546,6 +546,28 @@ open class HybridNitroInputViewSpec_cxx {
}
}
+ public final var tabularNums: Bool {
+ @inline(__always)
+ get {
+ return self.__implementation.tabularNums
+ }
+ @inline(__always)
+ set {
+ self.__implementation.tabularNums = newValue
+ }
+ }
+
+ public final var slotDigits: Bool {
+ @inline(__always)
+ get {
+ return self.__implementation.slotDigits
+ }
+ @inline(__always)
+ set {
+ self.__implementation.slotDigits = newValue
+ }
+ }
+
public final var prefixSpacing: Double {
@inline(__always)
get {
diff --git a/packages/react-native-nitro-input/nitrogen/generated/ios/swift/NitroInputSizing.swift b/packages/react-native-nitro-input/nitrogen/generated/ios/swift/NitroInputSizing.swift
index 1fe6888..deebbd7 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/ios/swift/NitroInputSizing.swift
+++ b/packages/react-native-nitro-input/nitrogen/generated/ios/swift/NitroInputSizing.swift
@@ -18,7 +18,7 @@ public extension NitroInputSizing {
/**
* Create a new instance of `NitroInputSizing`.
*/
- init(text: String, mode: NitroInputMode, plain: Bool, multiline: Bool, secureTextEntry: Bool, maxLength: Double, fractionDigits: Double, maxIntegerDigits: Double, groupingSeparator: String, decimalSeparator: String, prefix: String, suffix: String, signPlacement: NitroInputSignPlacement, placeholder: String, mask: String, maskNotations: [NitroInputNotation], maskAutocomplete: Bool, maskAutoSkip: Bool, maskAffinityFormats: [String], maskAffinityStrategy: NitroInputMaskAffinity, maskTextCase: NitroInputTextCase, maskCharacterMap: [NitroInputCharacterMapping], maskSlotPlaceholder: String, keepPlaceholder: Bool, textSize: Double, textLineHeight: Double, prefixFontSize: Double, suffixFontSize: Double, textWeight: Double, textFamily: String, textLetterSpacing: Double, prefixSpacing: Double, suffixSpacing: Double, allowFontScaling: Bool, maxFontSizeMultiplier: Double, variant: NitroInputVariant, label: String, labelBehavior: NitroInputLabelBehavior, labelFontSize: Double, cornerRadius: Double) {
+ init(text: String, mode: NitroInputMode, plain: Bool, multiline: Bool, secureTextEntry: Bool, maxLength: Double, fractionDigits: Double, maxIntegerDigits: Double, groupingSeparator: String, decimalSeparator: String, prefix: String, suffix: String, signPlacement: NitroInputSignPlacement, placeholder: String, mask: String, maskNotations: [NitroInputNotation], maskAutocomplete: Bool, maskAutoSkip: Bool, maskAffinityFormats: [String], maskAffinityStrategy: NitroInputMaskAffinity, maskTextCase: NitroInputTextCase, maskCharacterMap: [NitroInputCharacterMapping], maskSlotPlaceholder: String, keepPlaceholder: Bool, textSize: Double, textLineHeight: Double, prefixFontSize: Double, suffixFontSize: Double, textWeight: Double, textFamily: String, textLetterSpacing: Double, tabularNums: Bool, slotDigits: Bool, prefixSpacing: Double, suffixSpacing: Double, allowFontScaling: Bool, maxFontSizeMultiplier: Double, variant: NitroInputVariant, label: String, labelBehavior: NitroInputLabelBehavior, labelFontSize: Double, cornerRadius: Double) {
self.init(std.string(text), mode, plain, multiline, secureTextEntry, maxLength, fractionDigits, maxIntegerDigits, std.string(groupingSeparator), std.string(decimalSeparator), std.string(prefix), std.string(suffix), signPlacement, std.string(placeholder), std.string(mask), { () -> bridge.std__vector_NitroInputNotation_ in
var __vector = bridge.create_std__vector_NitroInputNotation_(maskNotations.count)
for __item in maskNotations {
@@ -37,7 +37,7 @@ public extension NitroInputSizing {
__vector.push_back(__item)
}
return __vector
- }(), std.string(maskSlotPlaceholder), keepPlaceholder, textSize, textLineHeight, prefixFontSize, suffixFontSize, textWeight, std.string(textFamily), textLetterSpacing, prefixSpacing, suffixSpacing, allowFontScaling, maxFontSizeMultiplier, variant, std.string(label), labelBehavior, labelFontSize, cornerRadius)
+ }(), std.string(maskSlotPlaceholder), keepPlaceholder, textSize, textLineHeight, prefixFontSize, suffixFontSize, textWeight, std.string(textFamily), textLetterSpacing, tabularNums, slotDigits, prefixSpacing, suffixSpacing, allowFontScaling, maxFontSizeMultiplier, variant, std.string(label), labelBehavior, labelFontSize, cornerRadius)
}
@inline(__always)
@@ -195,6 +195,16 @@ public extension NitroInputSizing {
return self.__textLetterSpacing
}
+ @inline(__always)
+ var tabularNums: Bool {
+ return self.__tabularNums
+ }
+
+ @inline(__always)
+ var slotDigits: Bool {
+ return self.__slotDigits
+ }
+
@inline(__always)
var prefixSpacing: Double {
return self.__prefixSpacing
diff --git a/packages/react-native-nitro-input/nitrogen/generated/shared/c++/HybridNitroInputViewSpec.cpp b/packages/react-native-nitro-input/nitrogen/generated/shared/c++/HybridNitroInputViewSpec.cpp
index 44e7f97..41fb908 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/shared/c++/HybridNitroInputViewSpec.cpp
+++ b/packages/react-native-nitro-input/nitrogen/generated/shared/c++/HybridNitroInputViewSpec.cpp
@@ -88,6 +88,10 @@ namespace margelo::nitro::nitroinput {
prototype.registerHybridSetter("suffixAlign", &HybridNitroInputViewSpec::setSuffixAlign);
prototype.registerHybridGetter("textLetterSpacing", &HybridNitroInputViewSpec::getTextLetterSpacing);
prototype.registerHybridSetter("textLetterSpacing", &HybridNitroInputViewSpec::setTextLetterSpacing);
+ prototype.registerHybridGetter("tabularNums", &HybridNitroInputViewSpec::getTabularNums);
+ prototype.registerHybridSetter("tabularNums", &HybridNitroInputViewSpec::setTabularNums);
+ prototype.registerHybridGetter("slotDigits", &HybridNitroInputViewSpec::getSlotDigits);
+ prototype.registerHybridSetter("slotDigits", &HybridNitroInputViewSpec::setSlotDigits);
prototype.registerHybridGetter("prefixSpacing", &HybridNitroInputViewSpec::getPrefixSpacing);
prototype.registerHybridSetter("prefixSpacing", &HybridNitroInputViewSpec::setPrefixSpacing);
prototype.registerHybridGetter("suffixSpacing", &HybridNitroInputViewSpec::getSuffixSpacing);
diff --git a/packages/react-native-nitro-input/nitrogen/generated/shared/c++/HybridNitroInputViewSpec.hpp b/packages/react-native-nitro-input/nitrogen/generated/shared/c++/HybridNitroInputViewSpec.hpp
index 9e84242..7cf9a66 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/shared/c++/HybridNitroInputViewSpec.hpp
+++ b/packages/react-native-nitro-input/nitrogen/generated/shared/c++/HybridNitroInputViewSpec.hpp
@@ -174,6 +174,10 @@ namespace margelo::nitro::nitroinput {
virtual void setSuffixAlign(NitroInputAffixAlign suffixAlign) = 0;
virtual double getTextLetterSpacing() = 0;
virtual void setTextLetterSpacing(double textLetterSpacing) = 0;
+ virtual bool getTabularNums() = 0;
+ virtual void setTabularNums(bool tabularNums) = 0;
+ virtual bool getSlotDigits() = 0;
+ virtual void setSlotDigits(bool slotDigits) = 0;
virtual double getPrefixSpacing() = 0;
virtual void setPrefixSpacing(double prefixSpacing) = 0;
virtual double getSuffixSpacing() = 0;
diff --git a/packages/react-native-nitro-input/nitrogen/generated/shared/c++/NitroInputSizing.hpp b/packages/react-native-nitro-input/nitrogen/generated/shared/c++/NitroInputSizing.hpp
index 580cab6..ecb0cbd 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/shared/c++/NitroInputSizing.hpp
+++ b/packages/react-native-nitro-input/nitrogen/generated/shared/c++/NitroInputSizing.hpp
@@ -94,6 +94,8 @@ namespace margelo::nitro::nitroinput {
double textWeight SWIFT_PRIVATE;
std::string textFamily SWIFT_PRIVATE;
double textLetterSpacing SWIFT_PRIVATE;
+ bool tabularNums SWIFT_PRIVATE;
+ bool slotDigits SWIFT_PRIVATE;
double prefixSpacing SWIFT_PRIVATE;
double suffixSpacing SWIFT_PRIVATE;
bool allowFontScaling SWIFT_PRIVATE;
@@ -106,7 +108,7 @@ namespace margelo::nitro::nitroinput {
public:
NitroInputSizing() = default;
- explicit NitroInputSizing(std::string text, NitroInputMode mode, bool plain, bool multiline, bool secureTextEntry, double maxLength, double fractionDigits, double maxIntegerDigits, std::string groupingSeparator, std::string decimalSeparator, std::string prefix, std::string suffix, NitroInputSignPlacement signPlacement, std::string placeholder, std::string mask, std::vector maskNotations, bool maskAutocomplete, bool maskAutoSkip, std::vector maskAffinityFormats, NitroInputMaskAffinity maskAffinityStrategy, NitroInputTextCase maskTextCase, std::vector maskCharacterMap, std::string maskSlotPlaceholder, bool keepPlaceholder, double textSize, double textLineHeight, double prefixFontSize, double suffixFontSize, double textWeight, std::string textFamily, double textLetterSpacing, double prefixSpacing, double suffixSpacing, bool allowFontScaling, double maxFontSizeMultiplier, NitroInputVariant variant, std::string label, NitroInputLabelBehavior labelBehavior, double labelFontSize, double cornerRadius): text(text), mode(mode), plain(plain), multiline(multiline), secureTextEntry(secureTextEntry), maxLength(maxLength), fractionDigits(fractionDigits), maxIntegerDigits(maxIntegerDigits), groupingSeparator(groupingSeparator), decimalSeparator(decimalSeparator), prefix(prefix), suffix(suffix), signPlacement(signPlacement), placeholder(placeholder), mask(mask), maskNotations(maskNotations), maskAutocomplete(maskAutocomplete), maskAutoSkip(maskAutoSkip), maskAffinityFormats(maskAffinityFormats), maskAffinityStrategy(maskAffinityStrategy), maskTextCase(maskTextCase), maskCharacterMap(maskCharacterMap), maskSlotPlaceholder(maskSlotPlaceholder), keepPlaceholder(keepPlaceholder), textSize(textSize), textLineHeight(textLineHeight), prefixFontSize(prefixFontSize), suffixFontSize(suffixFontSize), textWeight(textWeight), textFamily(textFamily), textLetterSpacing(textLetterSpacing), prefixSpacing(prefixSpacing), suffixSpacing(suffixSpacing), allowFontScaling(allowFontScaling), maxFontSizeMultiplier(maxFontSizeMultiplier), variant(variant), label(label), labelBehavior(labelBehavior), labelFontSize(labelFontSize), cornerRadius(cornerRadius) {}
+ explicit NitroInputSizing(std::string text, NitroInputMode mode, bool plain, bool multiline, bool secureTextEntry, double maxLength, double fractionDigits, double maxIntegerDigits, std::string groupingSeparator, std::string decimalSeparator, std::string prefix, std::string suffix, NitroInputSignPlacement signPlacement, std::string placeholder, std::string mask, std::vector maskNotations, bool maskAutocomplete, bool maskAutoSkip, std::vector maskAffinityFormats, NitroInputMaskAffinity maskAffinityStrategy, NitroInputTextCase maskTextCase, std::vector maskCharacterMap, std::string maskSlotPlaceholder, bool keepPlaceholder, double textSize, double textLineHeight, double prefixFontSize, double suffixFontSize, double textWeight, std::string textFamily, double textLetterSpacing, bool tabularNums, bool slotDigits, double prefixSpacing, double suffixSpacing, bool allowFontScaling, double maxFontSizeMultiplier, NitroInputVariant variant, std::string label, NitroInputLabelBehavior labelBehavior, double labelFontSize, double cornerRadius): text(text), mode(mode), plain(plain), multiline(multiline), secureTextEntry(secureTextEntry), maxLength(maxLength), fractionDigits(fractionDigits), maxIntegerDigits(maxIntegerDigits), groupingSeparator(groupingSeparator), decimalSeparator(decimalSeparator), prefix(prefix), suffix(suffix), signPlacement(signPlacement), placeholder(placeholder), mask(mask), maskNotations(maskNotations), maskAutocomplete(maskAutocomplete), maskAutoSkip(maskAutoSkip), maskAffinityFormats(maskAffinityFormats), maskAffinityStrategy(maskAffinityStrategy), maskTextCase(maskTextCase), maskCharacterMap(maskCharacterMap), maskSlotPlaceholder(maskSlotPlaceholder), keepPlaceholder(keepPlaceholder), textSize(textSize), textLineHeight(textLineHeight), prefixFontSize(prefixFontSize), suffixFontSize(suffixFontSize), textWeight(textWeight), textFamily(textFamily), textLetterSpacing(textLetterSpacing), tabularNums(tabularNums), slotDigits(slotDigits), prefixSpacing(prefixSpacing), suffixSpacing(suffixSpacing), allowFontScaling(allowFontScaling), maxFontSizeMultiplier(maxFontSizeMultiplier), variant(variant), label(label), labelBehavior(labelBehavior), labelFontSize(labelFontSize), cornerRadius(cornerRadius) {}
public:
friend bool operator==(const NitroInputSizing& lhs, const NitroInputSizing& rhs) = default;
@@ -153,6 +155,8 @@ namespace margelo::nitro {
JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "textWeight"))),
JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "textFamily"))),
JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "textLetterSpacing"))),
+ JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "tabularNums"))),
+ JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "slotDigits"))),
JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "prefixSpacing"))),
JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "suffixSpacing"))),
JSIConverter::fromJSI(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "allowFontScaling"))),
@@ -197,6 +201,8 @@ namespace margelo::nitro {
obj.setProperty(runtime, PropNameIDCache::get(runtime, "textWeight"), JSIConverter::toJSI(runtime, arg.textWeight));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "textFamily"), JSIConverter::toJSI(runtime, arg.textFamily));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "textLetterSpacing"), JSIConverter::toJSI(runtime, arg.textLetterSpacing));
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "tabularNums"), JSIConverter::toJSI(runtime, arg.tabularNums));
+ obj.setProperty(runtime, PropNameIDCache::get(runtime, "slotDigits"), JSIConverter::toJSI(runtime, arg.slotDigits));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "prefixSpacing"), JSIConverter::toJSI(runtime, arg.prefixSpacing));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "suffixSpacing"), JSIConverter::toJSI(runtime, arg.suffixSpacing));
obj.setProperty(runtime, PropNameIDCache::get(runtime, "allowFontScaling"), JSIConverter::toJSI(runtime, arg.allowFontScaling));
@@ -247,6 +253,8 @@ namespace margelo::nitro {
if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "textWeight")))) return false;
if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "textFamily")))) return false;
if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "textLetterSpacing")))) return false;
+ if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "tabularNums")))) return false;
+ if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "slotDigits")))) return false;
if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "prefixSpacing")))) return false;
if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "suffixSpacing")))) return false;
if (!JSIConverter::canConvert(runtime, obj.getProperty(runtime, PropNameIDCache::get(runtime, "allowFontScaling")))) return false;
diff --git a/packages/react-native-nitro-input/nitrogen/generated/shared/c++/views/HybridNitroInputViewComponent.cpp b/packages/react-native-nitro-input/nitrogen/generated/shared/c++/views/HybridNitroInputViewComponent.cpp
index df1702e..067d442 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/shared/c++/views/HybridNitroInputViewComponent.cpp
+++ b/packages/react-native-nitro-input/nitrogen/generated/shared/c++/views/HybridNitroInputViewComponent.cpp
@@ -57,6 +57,8 @@ namespace margelo::nitro::nitroinput::views {
prefixAlign(nitro::ReactProp::fromRawValue("NitroInputView", "prefixAlign", rawProps, sourceProps.prefixAlign)),
suffixAlign(nitro::ReactProp::fromRawValue("NitroInputView", "suffixAlign", rawProps, sourceProps.suffixAlign)),
textLetterSpacing(nitro::ReactProp::fromRawValue("NitroInputView", "textLetterSpacing", rawProps, sourceProps.textLetterSpacing)),
+ tabularNums(nitro::ReactProp::fromRawValue("NitroInputView", "tabularNums", rawProps, sourceProps.tabularNums)),
+ slotDigits(nitro::ReactProp::fromRawValue("NitroInputView", "slotDigits", rawProps, sourceProps.slotDigits)),
prefixSpacing(nitro::ReactProp::fromRawValue("NitroInputView", "prefixSpacing", rawProps, sourceProps.prefixSpacing)),
suffixSpacing(nitro::ReactProp::fromRawValue("NitroInputView", "suffixSpacing", rawProps, sourceProps.suffixSpacing)),
prefixOffset(nitro::ReactProp::fromRawValue("NitroInputView", "prefixOffset", rawProps, sourceProps.prefixOffset)),
@@ -170,6 +172,8 @@ namespace margelo::nitro::nitroinput::views {
case hashString("prefixAlign"): return true;
case hashString("suffixAlign"): return true;
case hashString("textLetterSpacing"): return true;
+ case hashString("tabularNums"): return true;
+ case hashString("slotDigits"): return true;
case hashString("prefixSpacing"): return true;
case hashString("suffixSpacing"): return true;
case hashString("prefixOffset"): return true;
diff --git a/packages/react-native-nitro-input/nitrogen/generated/shared/c++/views/HybridNitroInputViewComponent.hpp b/packages/react-native-nitro-input/nitrogen/generated/shared/c++/views/HybridNitroInputViewComponent.hpp
index 3eb41d3..51ebdcf 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/shared/c++/views/HybridNitroInputViewComponent.hpp
+++ b/packages/react-native-nitro-input/nitrogen/generated/shared/c++/views/HybridNitroInputViewComponent.hpp
@@ -99,6 +99,8 @@ namespace margelo::nitro::nitroinput::views {
nitro::ReactProp prefixAlign;
nitro::ReactProp suffixAlign;
nitro::ReactProp textLetterSpacing;
+ nitro::ReactProp tabularNums;
+ nitro::ReactProp slotDigits;
nitro::ReactProp prefixSpacing;
nitro::ReactProp suffixSpacing;
nitro::ReactProp prefixOffset;
@@ -212,6 +214,8 @@ namespace margelo::nitro::nitroinput::views {
prefixAlign.hasSameValue(other.prefixAlign) &&
suffixAlign.hasSameValue(other.suffixAlign) &&
textLetterSpacing.hasSameValue(other.textLetterSpacing) &&
+ tabularNums.hasSameValue(other.tabularNums) &&
+ slotDigits.hasSameValue(other.slotDigits) &&
prefixSpacing.hasSameValue(other.prefixSpacing) &&
suffixSpacing.hasSameValue(other.suffixSpacing) &&
prefixOffset.hasSameValue(other.prefixOffset) &&
@@ -326,6 +330,8 @@ namespace margelo::nitro::nitroinput::views {
prefixAlign.isProvided() ||
suffixAlign.isProvided() ||
textLetterSpacing.isProvided() ||
+ tabularNums.isProvided() ||
+ slotDigits.isProvided() ||
prefixSpacing.isProvided() ||
suffixSpacing.isProvided() ||
prefixOffset.isProvided() ||
diff --git a/packages/react-native-nitro-input/nitrogen/generated/shared/json/NitroInputViewConfig.json b/packages/react-native-nitro-input/nitrogen/generated/shared/json/NitroInputViewConfig.json
index a92dfbf..65895df 100644
--- a/packages/react-native-nitro-input/nitrogen/generated/shared/json/NitroInputViewConfig.json
+++ b/packages/react-native-nitro-input/nitrogen/generated/shared/json/NitroInputViewConfig.json
@@ -41,6 +41,8 @@
"prefixAlign": true,
"suffixAlign": true,
"textLetterSpacing": true,
+ "tabularNums": true,
+ "slotDigits": true,
"prefixSpacing": true,
"suffixSpacing": true,
"prefixOffset": true,
diff --git a/packages/react-native-nitro-input/src/NitroInput.tsx b/packages/react-native-nitro-input/src/NitroInput.tsx
index fcced0e..f79c0fa 100644
--- a/packages/react-native-nitro-input/src/NitroInput.tsx
+++ b/packages/react-native-nitro-input/src/NitroInput.tsx
@@ -445,6 +445,17 @@ export interface NitroInputProps extends Omit(
prefixAlign,
suffixAlign,
letterSpacing: letterSpacingProp,
+ tabularNums,
prefixSpacing,
suffixSpacing,
prefixOffset,
@@ -1537,6 +1549,8 @@ export const NitroInput = forwardRef(
textWeight: numericWeight,
textFamily: fontFamily ?? '',
textLetterSpacing: letterSpacing ?? 0,
+ tabularNums: tabularNums ?? !flat?.fontVariant?.includes('proportional-nums'),
+ slotDigits: tabularNums === true,
prefixSpacing: prefixSpacing ?? Infinity,
suffixSpacing: suffixSpacing ?? Infinity,
allowFontScaling: allowFontScaling ?? false,
diff --git a/packages/react-native-nitro-input/src/__tests__/Sizing.test.tsx b/packages/react-native-nitro-input/src/__tests__/Sizing.test.tsx
index 894eba4..fdc4df9 100644
--- a/packages/react-native-nitro-input/src/__tests__/Sizing.test.tsx
+++ b/packages/react-native-nitro-input/src/__tests__/Sizing.test.tsx
@@ -86,6 +86,22 @@ describe('the size before the view reports', () => {
expect(mockMeasure).toHaveBeenCalledTimes(2)
})
+ it("is measured with the font's tabular digits, slotted only when tabularNums asks", () => {
+ // [tnum, slots] as the view and the measure get them.
+ const digits = (element: React.ReactElement) => {
+ const native = nativeProps(render(element))
+ const sizing = mockMeasure.mock.calls.at(-1)![0]
+ expect([sizing.tabularNums, sizing.slotDigits]).toEqual([native.tabularNums, native.slotDigits])
+ return [native.tabularNums, native.slotDigits]
+ }
+ expect(digits()).toEqual([true, false])
+ expect(digits()).toEqual([true, true])
+ expect(digits()).toEqual([false, false])
+ expect(digits()).toEqual([false, false])
+ // The prop wins over the style, as the typography props do.
+ expect(digits()).toEqual([true, true])
+ })
+
it('is not measured for a wrapping field or one whose style sets both sides', () => {
render()
render()
diff --git a/packages/react-native-nitro-input/src/specs/NitroInput.nitro.ts b/packages/react-native-nitro-input/src/specs/NitroInput.nitro.ts
index 5649be0..aba6ead 100644
--- a/packages/react-native-nitro-input/src/specs/NitroInput.nitro.ts
+++ b/packages/react-native-nitro-input/src/specs/NitroInput.nitro.ts
@@ -268,6 +268,10 @@ export interface NitroInputProps extends HybridViewProps {
suffixAlign: NitroInputAffixAlign
/** Points added after every glyph, like `Text`'s `letterSpacing`; an affix gets it in proportion to its size. */
textLetterSpacing: number
+ /** The font's tabular figures (`tnum`) rather than its proportional ones. */
+ tabularNums: boolean
+ /** Every digit laid out as wide as the widest, centred: tabular figures for a font without them. */
+ slotDigits: boolean
/** Points between the prefix and the text, in place of the letter spacing there. Not finite: the letter spacing. */
prefixSpacing: number
/** Points between the text and the suffix, in place of the letter spacing there. Not finite: the letter spacing. */
diff --git a/packages/react-native-nitro-input/src/specs/NitroInputMeasure.nitro.ts b/packages/react-native-nitro-input/src/specs/NitroInputMeasure.nitro.ts
index b0d4951..6c600f9 100644
--- a/packages/react-native-nitro-input/src/specs/NitroInputMeasure.nitro.ts
+++ b/packages/react-native-nitro-input/src/specs/NitroInputMeasure.nitro.ts
@@ -47,6 +47,8 @@ export interface NitroInputSizing {
textWeight: number
textFamily: string
textLetterSpacing: number
+ tabularNums: boolean
+ slotDigits: boolean
prefixSpacing: number
suffixSpacing: number
allowFontScaling: boolean