@@ -1195,6 +1195,234 @@ internal object TextLayoutManager {
11951195 return ret
11961196 }
11971197
1198+ /* *
1199+ * Returns the bounding rectangles for all text fragments that belong to the specified react tag.
1200+ * This is useful for getting the visual boundaries of nested <Text> components within a
1201+ * paragraph.
1202+ *
1203+ * @param preparedLayout The prepared text layout containing the layout and react tags
1204+ * @param targetReactTag The react tag of the TextShadowNode to get rects for
1205+ * @return A FloatArray containing [x, y, width, height] for each fragment rect, or empty array if
1206+ * no fragments match the tag
1207+ */
1208+ @JvmStatic
1209+ fun getFragmentRectsForReactTag (
1210+ preparedLayout : PreparedLayout ,
1211+ targetReactTag : Int ,
1212+ ): FloatArray {
1213+ val layout = preparedLayout.layout
1214+ val text = layout.text as ? Spanned ? : return floatArrayOf()
1215+ val reactTags = preparedLayout.reactTags
1216+ val verticalOffset = preparedLayout.verticalOffset
1217+ val maximumNumberOfLines = preparedLayout.maximumNumberOfLines
1218+
1219+ val calculatedLineCount = calculateLineCount(layout, maximumNumberOfLines)
1220+ val retList = ArrayList <Float >()
1221+
1222+ // Find all fragments with the matching react tag
1223+ val fragmentIndexSpans = text.getSpans(0 , text.length, ReactFragmentIndexSpan ::class .java)
1224+
1225+ for (span in fragmentIndexSpans) {
1226+ val fragmentIndex = span.fragmentIndex
1227+ if (
1228+ fragmentIndex >= 0 &&
1229+ fragmentIndex < reactTags.size &&
1230+ reactTags[fragmentIndex] == targetReactTag
1231+ ) {
1232+ // This fragment belongs to the target TextShadowNode
1233+ val start = text.getSpanStart(span)
1234+ val end = text.getSpanEnd(span)
1235+
1236+ addRectsForSpanRange(layout, text, start, end, verticalOffset, calculatedLineCount, retList)
1237+ }
1238+ }
1239+
1240+ val ret = FloatArray (retList.size)
1241+ for (i in retList.indices) {
1242+ ret[i] = retList[i]
1243+ }
1244+ return ret
1245+ }
1246+
1247+ /* *
1248+ * Returns the bounding rectangles for all text fragments that belong to the specified react tag.
1249+ * This method works with the legacy ReactTagSpan used when enablePreparedTextLayout is disabled.
1250+ *
1251+ * @param layout The Android text layout
1252+ * @param targetReactTag The react tag of the TextShadowNode to get rects for
1253+ * @param verticalOffset Vertical offset to apply to the rects
1254+ * @param maximumNumberOfLines Maximum number of lines to consider
1255+ * @return A FloatArray containing [x, y, width, height] for each fragment rect, or empty array if
1256+ * no fragments match the tag
1257+ */
1258+ @JvmStatic
1259+ fun getFragmentRectsForReactTagFromLayout (
1260+ layout : Layout ,
1261+ targetReactTag : Int ,
1262+ verticalOffset : Float ,
1263+ maximumNumberOfLines : Int ,
1264+ ): FloatArray {
1265+ val text = layout.text as ? Spanned ? : return floatArrayOf()
1266+ val calculatedLineCount = calculateLineCount(layout, maximumNumberOfLines)
1267+ val retList = ArrayList <Float >()
1268+
1269+ // Find all ReactTagSpan spans with the matching react tag
1270+ val tagSpans = text.getSpans(0 , text.length, ReactTagSpan ::class .java)
1271+
1272+ for (span in tagSpans) {
1273+ if (span.reactTag == targetReactTag) {
1274+ val start = text.getSpanStart(span)
1275+ val end = text.getSpanEnd(span)
1276+
1277+ addRectsForSpanRange(layout, text, start, end, verticalOffset, calculatedLineCount, retList)
1278+ }
1279+ }
1280+
1281+ val ret = FloatArray (retList.size)
1282+ for (i in retList.indices) {
1283+ ret[i] = retList[i]
1284+ }
1285+ return ret
1286+ }
1287+
1288+ /* *
1289+ * Returns the bounding rectangles for all text fragments that belong to the specified react tag
1290+ * by creating a layout on-demand from the AttributedString. This is used as a fallback when
1291+ * PreparedLayout is not available (e.g., when enablePreparedTextLayout feature flag is disabled).
1292+ *
1293+ * @param context The Android context
1294+ * @param attributedString The attributed string containing the text fragments
1295+ * @param paragraphAttributes The paragraph attributes for layout
1296+ * @param width The layout width constraint
1297+ * @param height The layout height constraint
1298+ * @param targetReactTag The react tag of the TextShadowNode to get rects for
1299+ * @return A FloatArray containing [x, y, width, height] for each fragment rect, or empty array if
1300+ * no fragments match the tag
1301+ */
1302+ @JvmStatic
1303+ fun getFragmentRectsFromAttributedString (
1304+ context : Context ,
1305+ attributedString : ReadableMapBuffer ,
1306+ paragraphAttributes : ReadableMapBuffer ,
1307+ width : Float ,
1308+ height : Float ,
1309+ targetReactTag : Int ,
1310+ ): FloatArray {
1311+ val fragments = attributedString.getMapBuffer(AS_KEY_FRAGMENTS )
1312+ // Pass null for outputReactTags since we'll use ReactTagSpan directly
1313+ val text =
1314+ createSpannableFromAttributedString(
1315+ context,
1316+ fragments,
1317+ null , // reactTextViewManagerCallback
1318+ null , // outputReactTags - this ensures ReactTagSpan is used
1319+ )
1320+
1321+ val baseTextAttributes =
1322+ TextAttributeProps .fromMapBuffer(attributedString.getMapBuffer(AS_KEY_BASE_ATTRIBUTES ))
1323+
1324+ // Width and height from C++ are in DIPs, but createLayout expects pixels
1325+ // Convert to pixels for correct text wrapping
1326+ val widthInPx = width.dpToPx()
1327+ val heightInPx = height.dpToPx()
1328+
1329+ val layout =
1330+ createLayout(
1331+ text,
1332+ newPaintWithAttributes(baseTextAttributes, context),
1333+ attributedString,
1334+ paragraphAttributes,
1335+ widthInPx,
1336+ YogaMeasureMode .EXACTLY ,
1337+ heightInPx,
1338+ YogaMeasureMode .UNDEFINED ,
1339+ )
1340+
1341+ val maximumNumberOfLines =
1342+ if (paragraphAttributes.contains(PA_KEY_MAX_NUMBER_OF_LINES ))
1343+ paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES )
1344+ else ReactConstants .UNSET
1345+
1346+ val verticalOffset =
1347+ getVerticalOffset(
1348+ layout,
1349+ paragraphAttributes,
1350+ heightInPx,
1351+ YogaMeasureMode .UNDEFINED ,
1352+ maximumNumberOfLines,
1353+ )
1354+
1355+ return getFragmentRectsForReactTagFromLayout(
1356+ layout,
1357+ targetReactTag,
1358+ verticalOffset,
1359+ maximumNumberOfLines,
1360+ )
1361+ }
1362+
1363+ private fun addRectsForSpanRange (
1364+ layout : Layout ,
1365+ text : Spanned ,
1366+ start : Int ,
1367+ end : Int ,
1368+ verticalOffset : Float ,
1369+ calculatedLineCount : Int ,
1370+ retList : ArrayList <Float >,
1371+ ) {
1372+ if (start < 0 || end < 0 || start >= end) {
1373+ return
1374+ }
1375+
1376+ // Get the bounding rect for this text range
1377+ // We need to handle multi-line text fragments by getting rects for each line
1378+ val startLine = layout.getLineForOffset(start)
1379+ val endLine = layout.getLineForOffset(end - 1 )
1380+
1381+ for (line in startLine.. min(endLine, calculatedLineCount - 1 )) {
1382+ val lineStart = layout.getLineStart(line)
1383+ val lineEnd = layout.getLineEnd(line)
1384+
1385+ // Calculate the portion of this fragment on this line
1386+ val fragmentStartOnLine = max(start, lineStart)
1387+ val fragmentEndOnLine = min(end, lineEnd)
1388+
1389+ if (fragmentStartOnLine >= fragmentEndOnLine) {
1390+ continue
1391+ }
1392+
1393+ // Get the horizontal bounds
1394+ // For the left position, use getPrimaryHorizontal at the fragment start
1395+ val left = layout.getPrimaryHorizontal(fragmentStartOnLine)
1396+
1397+ // For the right position, we need to handle the case where the fragment
1398+ // ends at a line break. In this case, getPrimaryHorizontal at the end
1399+ // returns the same as at the start of the next line (or line start).
1400+ // Instead, we should use the line's right bound or the position just before
1401+ // the line end character.
1402+ val right: Float
1403+ if (fragmentEndOnLine >= lineEnd && lineEnd > lineStart) {
1404+ // Fragment goes to the end of the line - use the position before the newline
1405+ // or use getLineRight for the actual right edge of text on this line
1406+ right = layout.getLineRight(line)
1407+ } else {
1408+ right = layout.getPrimaryHorizontal(fragmentEndOnLine)
1409+ }
1410+
1411+ // Get the vertical bounds
1412+ val top = layout.getLineTop(line) + verticalOffset
1413+ val bottom = layout.getLineBottom(line) + verticalOffset
1414+
1415+ // Ensure left is less than right (RTL text handling)
1416+ val rectLeft = min(left, right)
1417+ val rectRight = max(left, right)
1418+
1419+ retList.add(rectLeft.pxToDp())
1420+ retList.add(top.pxToDp())
1421+ retList.add((rectRight - rectLeft).pxToDp())
1422+ retList.add((bottom - top).pxToDp())
1423+ }
1424+ }
1425+
11981426 private fun getVerticalOffset (
11991427 layout : Layout ,
12001428 paragraphAttributes : ReadableMapBuffer ,
0 commit comments