Repository navigation
Conversation
Adds `ref.measureRanges(ranges)`, which resolves with the rects the given ranges of text are drawn in: one per line, or a zero-width caret rect for a collapsed range, relative to the input's top-left corner. It lets apps draw overlays over the text being edited, such as other users' cursors and selections or comment highlights. Ranges use the same indexes as setSelection. Natively it is a `requestRangeRects` command answered by an `onRequestRangeRectsResult` event, following requestHTML/getHTML. iOS measures with the text view's NSLayoutManager and caretRectForPosition:, Android with the EditText's Layout, web with ProseMirror's DOM ranges. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved response-delivery and geometry errors can leave measurement promises pending or return incorrect overlays.
4 open issues
What changed in this PR
Adds EnrichedTextInput.measureRanges() across web, Android, and iOS so apps can position selection, caret, and annotation overlays.
Changes:
- Adds measurement implementations, native messaging, and exported geometry types.
- Documents the API and adds a native overlay demo.
- Adds web measurement tests and a dedicated test screen.
| File | Description |
|---|---|
| src/web/utils/measureRanges.ts | Measures DOM text and caret rectangles. |
| src/web/EnrichedTextInput.tsx | Exposes web measurement method. |
| src/types.ts | Defines public types and API contract. |
| src/spec/EnrichedTextInputNativeComponent.ts | Defines measurement command and response event. |
| src/native/EnrichedTextInput.tsx | Manages measurement requests and promises. |
| src/index.tsx | Exports geometry types. |
| src/index.native.tsx | Exports native geometry types. |
| ios/EnrichedTextInputView.mm | Implements iOS measurement. |
| docs/docs/api-reference/enriched-text-input.md | Documents measurement usage. |
| apps/example/src/screens/DevScreen.tsx | Demonstrates selection overlays. |
| apps/example-web/src/testScreens/TestMeasureRanges.tsx | Adds measurement test controls. |
| apps/example-web/src/RouteSelector.tsx | Registers the test route. |
| android/src/main/java/com/swmansion/enriched/textinput/events/OnRequestRangeRectsResultEvent.kt | Serializes measurement responses. |
| android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputViewManager.kt | Connects measurement commands and events. |
| android/src/main/java/com/swmansion/enriched/textinput/EnrichedTextInputView.kt | Implements Android measurement. |
| .playwright/tests/measureRanges.spec.ts | Tests web rectangle results. |
Review effort: Balanced
| } | ||
| if (from >= to) continue | ||
|
|
||
| val fromX = layout.getPrimaryHorizontal(from) |
| private val rects: List<RangeRect>, | ||
| private val experimentalSynchronousEvents: Boolean, | ||
| ) : Event<OnRequestRangeRectsResultEvent>(surfaceId, viewId) { | ||
| override fun getEventName(): String = EVENT_NAME |
| domRange.setEnd(end.node, end.offset); | ||
| for (const rect of Array.from(domRange.getClientRects())) { | ||
| if (rect.width > 0 && rect.height > 0) { | ||
| rects.push(toTextRect(rect, origin)); |
Comment on lines
+80
to
+84
| const from = nativePosToTiptapPos(doc, nativeStart); | ||
| if (nativeStart === nativeEnd) { | ||
| return caretRect(view, from, origin); | ||
| } | ||
| return rangeRects(view, from, nativePosToTiptapPos(doc, nativeEnd), origin); |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Adds
ref.measureRanges(ranges)toEnrichedTextInput. It reports where ranges of the text being edited are drawn, so apps can draw overlays over the input. The use case is collaborative editing: other users' carets and selections, and highlights of commented passages, drawn while the text is being edited. Today the input only exposes the measurements of the view as a whole, so there is no way to know where a character is.start === end.setSelection, i.e. the visible text without the internal zero-width spaces.TextRangeandTextRectare exported.Implementation
requestRangeRects(requestId, ranges: Int32[])command (ranges flattened as[start0, end0, …]) answered by anonRequestRangeRectsResultevent carrying a flat{ range, x, y, width, height }[]. It follows the existingrequestHTML/onRequestHtmlResult/getHTMLpattern: a requestId → promise map, rejected on unmount.EnrichedTextInputView.mm): maps visible to actual indexes with the existinggetActualIndex:, then walks the line fragments of the text view'sNSLayoutManager. Each rect takes the glyphs' horizontal extent fromboundingRectForGlyphRange:and the line's vertical extent from the line fragment's used rect, without the line break that ends a line. Rects are offset bytextContainerInsetand converted to the component view withconvertRect:toView:, which covers the content offset. A collapsed range usescaretRectForPosition:.EnrichedTextInputView.kt, newOnRequestRangeRectsResultEvent): maps indexes with the existinggetActualIndex, then for each line usesLayout.getPrimaryHorizontal,getLineTopandgetLineBottom. At a soft wrap it usesgetLineRight/getLineLeft, because the end offset there is also the start of the next line. The view's offsets aretotalPaddingLeft/Top - scrollX/Y, and values are converted withPixelUtil.toDIPFromPixel.src/web/utils/measureRanges.ts): maps indexes withnativePosToTiptapPos, then measures each text node of the range with a DOMRange.getClientRects(), so the boxes of block and mark elements are not reported. Inline images use their node's box, and a collapsed range usesview.coordsAtPos. Rects are relative to the.eti-editorelement.One thing I noticed but did not change: on iOS,
onChangeSelectionreports rawtextStorageindexes, whilesetSelection(and this method) take visible indexes. Android reports visible indexes for both. They differ only when the text contains internal zero-width spaces, such as empty list or quote lines.Test Plan
.playwright/tests/measureRanges.spec.tswith a/test-measure-rangesscreen in example-web. It checks result order, the zero-width caret rect, one rect per wrapped line, and a range across paragraphs. As a check that doesn't depend on the implementation, it clicks the centre of each measured rect and asserts that the caret lands inside the measured range. Passing locally (Chromium, Linux), together with the existingsetSelectionspec.DevScreen) now draws an orange overlay over the current selection or caret usingmeasureRanges. I built a release APK of the example (new arch, x86_64) and ran it on an Android 16 emulator (API 36.1, Pixel 6 profile). I checked that the overlay covers the native selection highlight exactly for a single word, a selection that wraps across lines, and a selection across paragraphs. I also checked a word after empty block-quote lines, which contain zero-width spaces, to confirm the index mapping, and that the caret rect matches the native caret.EventEmitters.handRCTComponentViewHelpers.h(structOnRequestRangeRectsResultRects,requestRangeRects:ranges:). It needs a run on a simulator. To verify, open the example's DevScreen, type text that wraps, then select a word, select all, and check that the orange overlay matches the native selection.yarn lint,yarn typecheck,yarn lint-clang,yarn testand the Android ktlintlintVerifypass.Screenshots / Videos
Android emulator: the orange
measureRangesoverlay sits on top of the native (deep sky blue) selection, which together look teal. Select all over a wrapped line plus a second paragraph gives three rects, one per line, tight to the glyphs. I can attach screenshots if useful.Compatibility
Checklist