feat: add layout-only Pretext native layout API#4
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7e1723790f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9cf7ba39ea
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| for segment in paragraph { | ||
| let baseOffset = (text as NSString).length | ||
| let breakBehavior = segment.breakBehavior.lowercased() | ||
| if segment.kind?.lowercased() == inlineSegmentKindBox || segment.boxId != nil { |
There was a problem hiding this comment.
Treat empty boxId as absent before box classification
The box classification in prepareInlineParagraphSeed treats any non-nil boxId as an inline box, which includes empty strings coming from materializeInlineParagraphs (boxId: "" is preserved). In that case iOS converts the segment into an object-replacement box and drops its text, producing incorrect widths/line breaks; Android normalizes empty strings to null, so this also creates cross-platform drift. Please require a non-empty boxId (or normalize empty strings to nil) before taking the box path.
Useful? React with 👍 / 👎.
| paragraph.compactMap { segment in | ||
| guard let segmentObject = segment as? [String: Any] else { | ||
| return nil |
There was a problem hiding this comment.
Reject invalid inline segment entries instead of dropping
materializeInlineParagraphs uses compactMap, so any non-object element in a paragraph (including null from sparse JS arrays after JSON serialization) is silently removed. That mutates segment count and UTF-16 offsets, yielding corrupted layout input without surfacing an error; Android currently throws for the same malformed payload, so behavior diverges by platform. This should fail fast rather than silently skipping entries.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 75025bfee1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| val canonicalLayoutEngine = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | ||
| LAYOUT_ENGINE_ANDROID_MEASURED_TEXT_LINE_BREAKER | ||
| } else { | ||
| LAYOUT_ENGINE_ANDROID_LEGACY_FALLBACK |
There was a problem hiding this comment.
Treat pre-Q static layout as canonical Android engine
For Android API 24–28, normal layout requests go through StaticLayoutLineLayout (which emits android_static_layout_compat), but this canonical-engine branch hardcodes all pre-Q devices to android_legacy_fallback. As a result, collectDriftKinds/line drift checks report engine_drift for the default pre-Q path, so diagnostics and benchmark parity signals are systematically wrong on supported older Android versions.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 60a7e1662c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| val width = AndroidTextUnits.toPx(max(0.0, segment.width ?: 0.0)) | ||
| val height = AndroidTextUnits.toPx(max(0.0, segment.height ?: 0.0)) | ||
| val baseline = AndroidTextUnits.toPx(segment.baseline ?: AndroidTextUnits.fromPx(height)) | ||
| .coerceIn(0.0, height) | ||
| return NativeInlineBox( |
There was a problem hiding this comment.
Reject inline box segments missing geometry
This path silently substitutes missing box metrics with zero values (width, height, baseline) instead of failing fast. Because InlineSegment does not require those fields when kind is "box", a caller can pass a box segment that type-checks but produces a 0x0 inline box and distorted line wrapping/box frames at runtime. Please validate box segments and throw when required geometry is absent.
Useful? React with 👍 / 👎.
| text = segmentJson.optString("text"), | ||
| breakBehavior = segmentJson.optString("breakBehavior"), |
There was a problem hiding this comment.
Validate inline segment string fields before coercion
Using optString here coerces non-string JSON values into strings (for example, numeric text becomes "123"), while the iOS parser treats non-strings as empty. The same malformed payload can therefore produce different token boundaries and layout results by platform without any error signal. This should reject non-string text/breakBehavior values instead of coercing them.
Useful? React with 👍 / 👎.
Summary
This PR turns
react-native-nitro-pretextinto a layout-only Pretext API for computing React Native text geometry before the visible UI renders.Pretext does not render text, does not expose a hidden measurement view, and does not derive height from
fontSize. Apps render with normal React Native UI after reading native text-engine metrics from the platform text engines.Major Release
major.measure(),measureBatch(),TextMeasure, public renderer components, native prepared renderer APIs, and raw prepared paragraph APIs from the package export surface.prepared.release()only; native prepared ids are hidden behind an opaque JS object.ts-pruneas a dev dead-export check and removed stale TypeScript/example exports found during the audit.Current Public API
Pretext.prepare()/ namedprepare()Pretext.layout()/ namedlayout()Pretext.usePretextLayout()/ namedusePretextLayout()layout()returns metrics by default and can optionally return line geometry, diagnostics, or rich inline box frames.usePretextLayout()owns prepare/layout/release from React lifecycle.Compatibility And Native Contract
*.>=0.81.0.react-native-nitro-modulespeer range:*.19.2.3, React Native0.85.0.MeasuredText + LineBreaker.CTTypesetter + CTLine.includeFontPaddingdefaults totruefor RN<Text>compatibility.MeasuredText + LineBreaker, and returns RN layout units.Examples And Docs
docs/api.mddocuments onlyPretext,prepare,layout, andusePretextLayoutwith props/types.examples/use-case/*maps to the API docs.examples/non-use-case/*shows the matching RN-only workaround with hidden measurement/callback state.benchmark/measured-layoutremains as the performance case study.Performance Snapshot
The headline comparison is the path an app would otherwise build with hidden RN
<Text>plusonLayoutto get a stable height before rendering the visible UI.129.16 ms1.55 ms98.8%127.61 ms83.3x174.95 ms8.59 ms95.1%166.36 ms20.4xHot relayout compute after
prepare()is a separate layout-only benchmark:<Text>median247.11 ms0.23 ms99.9%1074.4x54.55 ms0.06 ms99.9%909.2xThese are not claims that the final RN render surface is always faster. The Android debug AVD run reports Pretext visible surface median
85.01 msversus RN<Text>median54.55 ms; that surface timing is kept as context. The correctness/performance gate is the layout-only path plus required engine metadata.CI-Safe Local Validation
yarn nitrogenyarn fmt:checkyarn typecheckyarn lintyarn testyarn verify:api-examplesyarn verify:package-exportsyarn buildnpm pack --dry-run --ignore-scriptsyarn workspace react-native-nitro-pretext-example build:androidyarn workspace react-native-nitro-pretext-example build:iosAdditional latest checks run during review:
yarn ts-prune -p tsconfig.build.json -i 'src/index.ts|src/PublicTypes.ts|src/TextMeasure.native.ts' | grep -v '(used in module)' || trueyarn changeset status --since=mainManual Device Validation
Maestro is not part of CI because it depends on installed apps, simulator/device state, Metro, and route-level API/example contracts that may intentionally change.
Latest manual iOS Maestro benchmark, iPhone 16 simulator, debug app with Metro:
yarn benchmark:ios247.11 ms230.95 ms0.23 ms47.40 ms0/240mismatches35/240mismatchesLatest manual Android Maestro benchmark, Pixel_9_Pro AVD API 36, debug app with Metro:
MAESTRO_ANDROID_DEVICE_ID=emulator-5554 yarn benchmark:android54.55 ms85.01 ms0.06 ms140.01 msandroid_measured_text_line_breakerincludeFontPadding:true<Text>parity drift remains diagnostic because RN<Text>is a compatibility oracle, not the correctness sourceReview Focus
src/Pretext.tsandsrc/index.ts.src/Pretext.nitro.tsand platform implementations.