Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,60 @@ public float[] measurePreparedLayout(
getYogaMeasureMode(minHeight, maxHeight));
}

/**
* Returns the bounding rectangles for all text fragments that belong to the specified react tag.
* This is useful for getting the visual boundaries of nested {@code <Text>} components within a
* paragraph.
*
* @param preparedLayout The prepared text layout containing the layout and react tags
* @param targetReactTag The react tag of the TextShadowNode to get rects for
* @return A FloatArray containing [x, y, width, height] for each fragment rect, or empty array if
* no fragments match the tag
*/
@AnyThread
@ThreadConfined(ANY)
@UnstableReactNativeAPI
public float[] getFragmentRectsForReactTag(PreparedLayout preparedLayout, int targetReactTag) {
return TextLayoutManager.getFragmentRectsForReactTag(preparedLayout, targetReactTag);
}

/**
* Returns the bounding rectangles for all text fragments that belong to the specified react tag
* by creating a layout on-demand from the AttributedString. This is used as a fallback when
* PreparedLayout is not available (e.g., when enablePreparedTextLayout feature flag is disabled).
*
* @param surfaceId The surface ID to get context from
* @param attributedString The attributed string containing the text fragments
* @param paragraphAttributes The paragraph attributes for layout
* @param width The layout width constraint
* @param height The layout height constraint
* @param targetReactTag The react tag of the TextShadowNode to get rects for
* @return A FloatArray containing [x, y, width, height] for each fragment rect, or empty array if
* no fragments match the tag
*/
@AnyThread
@ThreadConfined(ANY)
@UnstableReactNativeAPI
public float[] getFragmentRectsFromAttributedString(
int surfaceId,
ReadableMapBuffer attributedString,
ReadableMapBuffer paragraphAttributes,
float width,
float height,
int targetReactTag) {
SurfaceMountingManager surfaceMountingManager = mMountingManager.getSurfaceManager(surfaceId);
Context context = surfaceMountingManager != null ? surfaceMountingManager.getContext() : null;
if (context == null) {
FLog.w(
TAG,
"Couldn't get context for surfaceId %d in getFragmentRectsFromAttributedString",
surfaceId);
return new float[0];
}
return TextLayoutManager.getFragmentRectsFromAttributedString(
context, attributedString, paragraphAttributes, width, height, targetReactTag);
}

/**
* @param surfaceId {@link int} surface ID
* @param defaultTextInputPadding {@link float[]} output parameter will contain the default theme
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1195,6 +1195,234 @@ internal object TextLayoutManager {
return ret
}

/**
* Returns the bounding rectangles for all text fragments that belong to the specified react tag.
* This is useful for getting the visual boundaries of nested <Text> components within a
* paragraph.
*
* @param preparedLayout The prepared text layout containing the layout and react tags
* @param targetReactTag The react tag of the TextShadowNode to get rects for
* @return A FloatArray containing [x, y, width, height] for each fragment rect, or empty array if
* no fragments match the tag
*/
@JvmStatic
fun getFragmentRectsForReactTag(
preparedLayout: PreparedLayout,
targetReactTag: Int,
): FloatArray {
val layout = preparedLayout.layout
val text = layout.text as? Spanned ?: return floatArrayOf()
val reactTags = preparedLayout.reactTags
val verticalOffset = preparedLayout.verticalOffset
val maximumNumberOfLines = preparedLayout.maximumNumberOfLines

val calculatedLineCount = calculateLineCount(layout, maximumNumberOfLines)
val retList = ArrayList<Float>()

// Find all fragments with the matching react tag
val fragmentIndexSpans = text.getSpans(0, text.length, ReactFragmentIndexSpan::class.java)

for (span in fragmentIndexSpans) {
val fragmentIndex = span.fragmentIndex
if (
fragmentIndex >= 0 &&
fragmentIndex < reactTags.size &&
reactTags[fragmentIndex] == targetReactTag
) {
// This fragment belongs to the target TextShadowNode
val start = text.getSpanStart(span)
val end = text.getSpanEnd(span)

addRectsForSpanRange(layout, text, start, end, verticalOffset, calculatedLineCount, retList)
}
}

val ret = FloatArray(retList.size)
for (i in retList.indices) {
ret[i] = retList[i]
}
return ret
}

/**
* Returns the bounding rectangles for all text fragments that belong to the specified react tag.
* This method works with the legacy ReactTagSpan used when enablePreparedTextLayout is disabled.
*
* @param layout The Android text layout
* @param targetReactTag The react tag of the TextShadowNode to get rects for
* @param verticalOffset Vertical offset to apply to the rects
* @param maximumNumberOfLines Maximum number of lines to consider
* @return A FloatArray containing [x, y, width, height] for each fragment rect, or empty array if
* no fragments match the tag
*/
@JvmStatic
fun getFragmentRectsForReactTagFromLayout(
layout: Layout,
targetReactTag: Int,
verticalOffset: Float,
maximumNumberOfLines: Int,
): FloatArray {
val text = layout.text as? Spanned ?: return floatArrayOf()
val calculatedLineCount = calculateLineCount(layout, maximumNumberOfLines)
val retList = ArrayList<Float>()

// Find all ReactTagSpan spans with the matching react tag
val tagSpans = text.getSpans(0, text.length, ReactTagSpan::class.java)

for (span in tagSpans) {
if (span.reactTag == targetReactTag) {
val start = text.getSpanStart(span)
val end = text.getSpanEnd(span)

addRectsForSpanRange(layout, text, start, end, verticalOffset, calculatedLineCount, retList)
}
}

val ret = FloatArray(retList.size)
for (i in retList.indices) {
ret[i] = retList[i]
}
return ret
}

/**
* Returns the bounding rectangles for all text fragments that belong to the specified react tag
* by creating a layout on-demand from the AttributedString. This is used as a fallback when
* PreparedLayout is not available (e.g., when enablePreparedTextLayout feature flag is disabled).
*
* @param context The Android context
* @param attributedString The attributed string containing the text fragments
* @param paragraphAttributes The paragraph attributes for layout
* @param width The layout width constraint
* @param height The layout height constraint
* @param targetReactTag The react tag of the TextShadowNode to get rects for
* @return A FloatArray containing [x, y, width, height] for each fragment rect, or empty array if
* no fragments match the tag
*/
@JvmStatic
fun getFragmentRectsFromAttributedString(
context: Context,
attributedString: ReadableMapBuffer,
paragraphAttributes: ReadableMapBuffer,
width: Float,
height: Float,
targetReactTag: Int,
): FloatArray {
val fragments = attributedString.getMapBuffer(AS_KEY_FRAGMENTS)
// Pass null for outputReactTags since we'll use ReactTagSpan directly
val text =
createSpannableFromAttributedString(
context,
fragments,
null, // reactTextViewManagerCallback
null, // outputReactTags - this ensures ReactTagSpan is used
)

val baseTextAttributes =
TextAttributeProps.fromMapBuffer(attributedString.getMapBuffer(AS_KEY_BASE_ATTRIBUTES))

// Width and height from C++ are in DIPs, but createLayout expects pixels
// Convert to pixels for correct text wrapping
val widthInPx = width.dpToPx()
val heightInPx = height.dpToPx()

val layout =
createLayout(
text,
newPaintWithAttributes(baseTextAttributes, context),
attributedString,
paragraphAttributes,
widthInPx,
YogaMeasureMode.EXACTLY,
heightInPx,
YogaMeasureMode.UNDEFINED,
)

val maximumNumberOfLines =
if (paragraphAttributes.contains(PA_KEY_MAX_NUMBER_OF_LINES))
paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES)
else ReactConstants.UNSET

val verticalOffset =
getVerticalOffset(
layout,
paragraphAttributes,
heightInPx,
YogaMeasureMode.UNDEFINED,
maximumNumberOfLines,
)

return getFragmentRectsForReactTagFromLayout(
layout,
targetReactTag,
verticalOffset,
maximumNumberOfLines,
)
}

private fun addRectsForSpanRange(
layout: Layout,
text: Spanned,
start: Int,
end: Int,
verticalOffset: Float,
calculatedLineCount: Int,
retList: ArrayList<Float>,
) {
if (start < 0 || end < 0 || start >= end) {
return
}

// Get the bounding rect for this text range
// We need to handle multi-line text fragments by getting rects for each line
val startLine = layout.getLineForOffset(start)
val endLine = layout.getLineForOffset(end - 1)

for (line in startLine..min(endLine, calculatedLineCount - 1)) {
val lineStart = layout.getLineStart(line)
val lineEnd = layout.getLineEnd(line)

// Calculate the portion of this fragment on this line
val fragmentStartOnLine = max(start, lineStart)
val fragmentEndOnLine = min(end, lineEnd)

if (fragmentStartOnLine >= fragmentEndOnLine) {
continue
}

// Get the horizontal bounds
// For the left position, use getPrimaryHorizontal at the fragment start
val left = layout.getPrimaryHorizontal(fragmentStartOnLine)

// For the right position, we need to handle the case where the fragment
// ends at a line break. In this case, getPrimaryHorizontal at the end
// returns the same as at the start of the next line (or line start).
// Instead, we should use the line's right bound or the position just before
// the line end character.
val right: Float
if (fragmentEndOnLine >= lineEnd && lineEnd > lineStart) {
// Fragment goes to the end of the line - use the position before the newline
// or use getLineRight for the actual right edge of text on this line
right = layout.getLineRight(line)
} else {
right = layout.getPrimaryHorizontal(fragmentEndOnLine)
}

// Get the vertical bounds
val top = layout.getLineTop(line) + verticalOffset
val bottom = layout.getLineBottom(line) + verticalOffset

// Ensure left is less than right (RTL text handling)
val rectLeft = min(left, right)
val rectRight = max(left, right)

retList.add(rectLeft.pxToDp())
retList.add(top.pxToDp())
retList.add((rectRight - rectLeft).pxToDp())
retList.add((bottom - top).pxToDp())
}
}

private fun getVerticalOffset(
layout: Layout,
paragraphAttributes: ReadableMapBuffer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,30 @@ NativeDOM::getBoundingClientRect(
return std::tuple{domRect.x, domRect.y, domRect.width, domRect.height};
}

std::vector<std::tuple<
/* x: */ double,
/* y: */ double,
/* width: */ double,
/* height: */ double>>
NativeDOM::getClientRects(
jsi::Runtime& rt,
std::shared_ptr<const ShadowNode> shadowNode) {
auto currentRevision =
getCurrentShadowTreeRevision(rt, shadowNode->getSurfaceId());
if (currentRevision == nullptr) {
return {};
}

auto domRects = dom::getClientRects(currentRevision, *shadowNode);

std::vector<std::tuple<double, double, double, double>> result;
result.reserve(domRects.size());
for (const auto& rect : domRects) {
result.emplace_back(rect.x, rect.y, rect.width, rect.height);
}
return result;
}

std::tuple</* width: */ int, /* height: */ int> NativeDOM::getInnerSize(
jsi::Runtime& rt,
std::shared_ptr<const ShadowNode> shadowNode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ class NativeDOM : public NativeDOMCxxSpec<NativeDOM> {
/* height: */ double>
getBoundingClientRect(jsi::Runtime &rt, std::shared_ptr<const ShadowNode> shadowNode, bool includeTransform);

std::vector<std::tuple<
/* x: */ double,
/* y: */ double,
/* width: */ double,
/* height: */ double>>
getClientRects(jsi::Runtime &rt, std::shared_ptr<const ShadowNode> shadowNode);

std::tuple</* width: */ int, /* height: */ int> getInnerSize(
jsi::Runtime &rt,
std::shared_ptr<const ShadowNode> shadowNode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,34 @@
cmake_minimum_required(VERSION 3.13)
set(CMAKE_VERBOSE_MAKEFILE on)

include(${REACT_COMMON_DIR}/cmake-utils/internal/react-native-platform-selector.cmake)
include(${REACT_COMMON_DIR}/cmake-utils/react-native-flags.cmake)

file(GLOB react_renderer_dom_SRC CONFIGURE_DEPENDS *.cpp)
react_native_android_selector(platform_SRC
platform/android/react/renderer/dom/*.cpp
platform/cxx/react/renderer/dom/*.cpp
)
file(GLOB react_renderer_dom_SRC CONFIGURE_DEPENDS *.cpp ${platform_SRC})

add_library(react_renderer_dom OBJECT ${react_renderer_dom_SRC})

target_include_directories(react_renderer_dom PUBLIC ${REACT_COMMON_DIR})
react_native_android_selector(platform_DIR
${CMAKE_CURRENT_SOURCE_DIR}/platform/android/
${CMAKE_CURRENT_SOURCE_DIR}/platform/cxx/)
target_include_directories(react_renderer_dom PUBLIC
${REACT_COMMON_DIR}
${platform_DIR})

react_native_android_selector(platform_DIR_PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/platform/android/react/renderer/dom/
${CMAKE_CURRENT_SOURCE_DIR}/platform/cxx/react/renderer/dom/)
target_include_directories(react_renderer_dom PRIVATE
${platform_DIR_PRIVATE})

target_link_libraries(react_renderer_dom
react_renderer_core
react_renderer_graphics
react_renderer_textlayoutmanager
rrc_root
rrc_text)
target_compile_reactnative_options(react_renderer_dom PRIVATE)
Expand Down
Loading
Loading