feat(react-native): baseline alignment + getTexMetrics()#139
Conversation
alignSelf:'baseline' (also center/flex-start/flex-end) now works in both host contexts, no new public prop: - Flex row (alignItems:'baseline'): the Fabric shadow node reports the drawn ink baseline via baseline() — fit-scale + centering gap + exact depth, snapped to the physical pixel grid. - Inside <Text>: the text engine pins the view bottom to the baseline and reserves the whole view height as line ASCENT — so for 'baseline' the measure reports the ascent-only box (height − depth), giving the line above flex-equal spacing, and the view draws natural-size ink anchored to the frame bottom with the descender overflowing below (iOS: unequal top/bottom constraint constants; Android: no-clip bottom-anchored draw). JS forwards alignSelf as an internal codegen prop (inlineAlign), gated by TextAncestorContext — native cannot detect the text host itself. center/start/end keep the full-height translate model. - iOS: compensates stock RN's missing font attribute on inline attachment runs at runtime (stockAttachmentSink), replacing a react-native patch; self-disabling on a fixed RN. - Android: RaTeXMetrics — sync natural metrics (width/height/depth) backed by the engine parse cache shared with measure and render.
…xMetrics)
ref.getTexMetrics() -> {depth, scale, width, height} | null, sync
(callable from useLayoutEffect). For hosts that lay out themselves
(markdown renderers with attachment overlays, canvas typesetters):
flex baseline and inline <Text> place the formula natively; everyone
else needs the number — the native analogue of KaTeX's .depth /
vertical-align: -depth on web.
- depth is DRAWN: view bottom edge -> ink baseline for the committed
frame (fit scale + centering gap applied), valid at any maxWidth
clamp. width/height are natural ink size; scale is the fit k.
- Backed by a new sync TurboModule (RaTeXModule) over the existing
RaTeXMeasure / RaTeXMetrics parse-cache-backed backends — an
on-screen formula never re-parses. Android passes the render color
(its cache is color-keyed, shared with measure/render); iOS metrics
are color-blind and ignore it.
- useImperativeHandle returns the genuine host instance augmented with
the method — host identity, measure(), findNodeHandle all survive.
Old arch: measure is async there; metrics fall back to natural size.
- Demo: 'ink baseline for custom hosts' card draws a ruler positioned
only from getTexMetrics().depth across several maxWidth clamps.
|
P.S. The flex and in- alignment paths cover simpler renderers well — you get per-span colors, fonts, and weights from plain RN — at the cost of the caveats above: the deep-descender overdraw in , and selection that can't cross a flex row. |
|
Thank you for the excellent and thorough implementation and documentation! This looks great overall. One small suggestion: should we use forwardRef to preserve compatibility with React 18.x? |
|
React 19 landed Dec 5, 2024. RN shipped it in 0.78 on Feb 19, 2025 - a year and a half ago. RN itself now marks everything below 0.84 as unsupported (https://reactnative.dev/versions). PS: My opinion - In the AI era, what excuses can people even have for sitting on an outdated RN or React? ;-) When all that stands between them and an upgrade is a single prompt. |
Couldn't agree more. 😄 |
closes #130
feat(react-native): baseline alignment +
getTexMetrics()Inline math should sit on the text baseline like a glyph. This PR makes
alignSelf: 'baseline'work in both host contexts RN offers, and exposes theformula's ink metrics for hosts that do their own text layout.
1. Flex row —
alignItems: 'baseline'Works perfectly — Yoga lines the formula's ink baseline up with the text
baseline, at any font size and under
maxWidthclamping. One platformlimitation (not this library's): the row is separate native nodes, so text
selection can't flow across it.
2. Inside
<Text>—alignSelf: 'baseline'The formula sits on the line's baseline like a glyph, line spacing matches
what a baseline flex row produces, and the paragraph stays real selectable
text. Same style as the flex case — no new public prop;
center/flex-start/flex-endwork too.Both cases are fully opt-in: no new props, and nothing changes unless you
set
alignSelf. They also cover whatInlineTeXcan't — per-span styling,nested components, and composing with an app-owned
<Text>tree (e.g. amarkdown renderer).
Known issue (bottom side, not fixable library-side). RN's inline-view
protocol has no notion of depth: a line reserves only its font's descender
below the baseline. A formula much deeper than the text keeps its correct
baseline but its tail can overdraw the next line — where a flex row (blue)
reserves the full depth. Everything above the baseline is exact. Fixing this
needs depth in RN's attachment protocol itself.
3.
getTexMetrics()— text engines without the limitationsThe two cases above are RN layout. A host that renders text itself — a
markdown renderer or editor built on TextKit (iOS) / Spannables (Android)
— has none of RN's inline limitations (real selection, real line breaking,
depth-aware lines), but it needs the formula's depth as a number to place
the attachment box. That's exactly what KaTeX ships on the web with every
formula (
.depth, emitted asvertical-align: -depth).On a mounted view — drawn metrics at the committed layout:
And the global function the ref method uses internally — natural metrics for
any formula, no view needed:
Both are sync (callable in
useLayoutEffect) and served from the parse cachethe measure/render passes share, so an on-screen formula never re-parses.
Cold parse is ~0.2 ms per formula (release builds); a cache hit is
microseconds.
This is enough to build your own
InlineTeX-style engine with none of thelimitations above. The screenshot below is such a host (TextKit/Spannables +
getTexMetrics): selectable, per-span styling, and the line holding the deepformula reserves its full depth — no overdraw:
Screenshots
All examples from
demo/react-native-expo(iPhone 17 Pro simulator / Pixel 9aemulator, RN 0.86, new arch). Amber = inline in
<Text>, blue = flexbaseline (ground truth), red = no alignment. Full-size images in
istarkov/RaTeX#1.
<Text>vs flex (full card)<Text>alignSelfoptionsgetTexMetrics()depth rulergetTexMetrics()perf cardDesign decisions
alignSelfstyle worksin both host contexts; native applies the placement itself.
at layout time; both forms are sync so they work inside
useLayoutEffect.scale + centering applied — apply
depthdirectly at the committed size);the global function returns natural metrics for engines that lay out
before mounting.
and metrics share one parsed representation per formula; no extra parses.
.depth/vertical-align: -depthon the web.Technical details (brief)
baseline()— drawn inkbaseline (fit scale ≤ 1 + centering gap + TeX depth), pixel-grid snapped.
<Text>: RN reserves an inline view's full height as line ascent,so for
'baseline'the node measures the ascent-only box (height − depth)and draws bottom-anchored, descender overflowing — line spacing matches the
flex row. JS forwards
alignSelfthrough an internal codegen prop(
inlineAlign) gated byTextAncestorContext.(inline attachment run has no font attribute); self-disabling on a fixed RN.
getTexMetricsis a thin sync TurboModule (RaTeXModule, old + new arch)over the existing
RaTeXMeasure/RaTeXMetricsbackends; the ref stays agenuine host instance (
measureetc. untouched).arch (metrics fall back to natural size on old arch, where
measureisasync).