diff --git a/packages/react-native/Libraries/Text/Text/RCTTextShadowView.h b/packages/react-native/Libraries/Text/Text/RCTTextShadowView.h index f3202b5fd37315..983f978b897606 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextShadowView.h +++ b/packages/react-native/Libraries/Text/Text/RCTTextShadowView.h @@ -20,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) BOOL adjustsFontSizeToFit; @property (nonatomic, assign) CGFloat minimumFontScale; @property (nonatomic, copy) RCTDirectEventBlock onTextLayout; +@property (nonatomic, copy) NSArray *textLayoutRegions; - (void)uiManagerWillPerformMounting; diff --git a/packages/react-native/Libraries/Text/Text/RCTTextViewManager.m b/packages/react-native/Libraries/Text/Text/RCTTextViewManager.m index 7b53c80d557acc..6adacfc38b2b64 100644 --- a/packages/react-native/Libraries/Text/Text/RCTTextViewManager.m +++ b/packages/react-native/Libraries/Text/Text/RCTTextViewManager.m @@ -32,6 +32,7 @@ @implementation RCTTextViewManager { RCT_REMAP_SHADOW_PROPERTY(minimumFontScale, minimumFontScale, CGFloat) RCT_EXPORT_SHADOW_PROPERTY(onTextLayout, RCTDirectEventBlock) +RCT_EXPORT_SHADOW_PROPERTY(textLayoutRegions, NSArray) RCT_EXPORT_VIEW_PROPERTY(selectable, BOOL) diff --git a/packages/react-native/Libraries/Text/TextNativeComponent.js b/packages/react-native/Libraries/Text/TextNativeComponent.js index 812f334806eb73..167d5e2e624a99 100644 --- a/packages/react-native/Libraries/Text/TextNativeComponent.js +++ b/packages/react-native/Libraries/Text/TextNativeComponent.js @@ -44,6 +44,7 @@ const textViewConfig = { minimumFontScale: true, textBreakStrategy: true, onTextLayout: true, + textLayoutRegions: true, onInlineViewLayout: true, dataDetectorType: true, android_hyphenationFrequency: true, diff --git a/packages/react-native/Libraries/Text/TextProps.js b/packages/react-native/Libraries/Text/TextProps.js index bb9348f6a55e4f..563c9488314fd0 100644 --- a/packages/react-native/Libraries/Text/TextProps.js +++ b/packages/react-native/Libraries/Text/TextProps.js @@ -168,6 +168,11 @@ export type TextProps = $ReadOnly<{| onMoveShouldSetResponder?: ?() => boolean, onTextLayout?: ?(event: TextLayoutEvent) => mixed, + /** + * Regions for text layout tracking + */ + textLayoutRegions?: ?$ReadOnlyArray<$ReadOnlyArray>, + /** * Defines how far your touch may move off of the button, before * deactivating the button. diff --git a/packages/react-native/Libraries/Types/CoreEventTypes.d.ts b/packages/react-native/Libraries/Types/CoreEventTypes.d.ts index 7aff083b07a2bf..e70c467cec4e90 100644 --- a/packages/react-native/Libraries/Types/CoreEventTypes.d.ts +++ b/packages/react-native/Libraries/Types/CoreEventTypes.d.ts @@ -33,11 +33,22 @@ interface TextLayoutLine { y: number; } +interface TextLayoutRegion { + text: string; + height: number; + width: number; + x: number; + y: number; + line: number; + region: number; +} + /** * @see TextProps.onTextLayout */ export interface TextLayoutEventData extends TargetedEvent { lines: TextLayoutLine[]; + regions: TextLayoutRegion[]; } // Similar to React.SyntheticEvent except for nativeEvent diff --git a/packages/react-native/Libraries/Types/CoreEventTypes.js b/packages/react-native/Libraries/Types/CoreEventTypes.js index a0234e95f26d16..bd9eb5cd8e017d 100644 --- a/packages/react-native/Libraries/Types/CoreEventTypes.js +++ b/packages/react-native/Libraries/Types/CoreEventTypes.js @@ -72,6 +72,16 @@ export type TextLayout = $ReadOnly<{| xHeight: number, |}>; +export type TextRegion = $ReadOnly<{| + text: string, + height: number, + width: number, + x: number, + y: number, + line: number, + region: number, +|}>; + export type LayoutEvent = SyntheticEvent< $ReadOnly<{| layout: Layout, @@ -81,6 +91,7 @@ export type LayoutEvent = SyntheticEvent< export type TextLayoutEvent = SyntheticEvent< $ReadOnly<{| lines: Array, + regions: Array, |}>, >; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index c785ad9b3075c6..72b95c787a04d4 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -51,6 +51,7 @@ import com.facebook.react.bridge.UiThreadUtil; import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.build.ReactBuildConfig; +import com.facebook.react.common.mapbuffer.MapBuffer; import com.facebook.react.common.mapbuffer.ReadableMapBuffer; import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.fabric.events.EventBeatManager; @@ -443,28 +444,35 @@ public void onCatalystInstanceDestroy() { } @SuppressWarnings("unused") - private NativeArray measureLines( - ReadableMap attributedString, ReadableMap paragraphAttributes, float width, float height) { - return (NativeArray) + private NativeMap measureLines( + ReadableMap attributedString, + ReadableMap paragraphAttributes, + float width, + float height, + MapBuffer textLayoutRegions) { + return (NativeMap) TextLayoutManager.measureLines( mReactApplicationContext, attributedString, paragraphAttributes, - PixelUtil.toPixelFromDIP(width)); + PixelUtil.toPixelFromDIP(width), + textLayoutRegions); } @SuppressWarnings("unused") - private NativeArray measureLinesMapBuffer( + private NativeMap measureLinesMapBuffer( ReadableMapBuffer attributedString, ReadableMapBuffer paragraphAttributes, float width, - float height) { - return (NativeArray) + float height, + ReadableMapBuffer textLayoutRegions) { + return (NativeMap) TextLayoutManagerMapBuffer.measureLines( mReactApplicationContext, attributedString, paragraphAttributes, - PixelUtil.toPixelFromDIP(width)); + PixelUtil.toPixelFromDIP(width), + textLayoutRegions); } @SuppressWarnings("unused") diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/FontMetricsUtil.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/FontMetricsUtil.java index f19a1b0425888d..019573d4fb4baa 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/FontMetricsUtil.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/FontMetricsUtil.java @@ -13,8 +13,10 @@ import android.text.TextPaint; import android.util.DisplayMetrics; import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.mapbuffer.MapBuffer; public class FontMetricsUtil { @@ -22,10 +24,11 @@ public class FontMetricsUtil { private static final String X_HEIGHT_MEASUREMENT_TEXT = "x"; private static final float AMPLIFICATION_FACTOR = 100; - public static WritableArray getFontMetrics( - CharSequence text, Layout layout, TextPaint paint, Context context) { + public static WritableMap getFontMetrics( + CharSequence text, Layout layout, TextPaint paint, Context context, MapBuffer textLayoutRegions) { DisplayMetrics dm = context.getResources().getDisplayMetrics(); WritableArray lines = Arguments.createArray(); + WritableArray regions = Arguments.createArray(); // To calculate xHeight and capHeight we have to render an "x" and "T" and manually measure // their height. // In order to get more precision than Android offers, we blow up the text size by 100 and @@ -41,6 +44,7 @@ public static WritableArray getFontMetrics( paintCopy.getTextBounds( X_HEIGHT_MEASUREMENT_TEXT, 0, X_HEIGHT_MEASUREMENT_TEXT.length(), xHeightBounds); double xHeight = xHeightBounds.height() / AMPLIFICATION_FACTOR / dm.density; + for (int i = 0; i < layout.getLineCount(); i++) { Rect bounds = new Rect(); layout.getLineBounds(i, bounds); @@ -56,8 +60,47 @@ public static WritableArray getFontMetrics( line.putDouble("xHeight", xHeight); line.putString( "text", text.subSequence(layout.getLineStart(i), layout.getLineEnd(i)).toString()); + lines.pushMap(line); } - return lines; + + for (int j = 0; j < textLayoutRegions.getCount(); j++) { + for (int i = 0; i < layout.getLineCount(); i++) { + Rect bounds = new Rect(); + layout.getLineBounds(i, bounds); + + MapBuffer textLayoutRegion = textLayoutRegions.getMapBuffer(j); + + int offset = layout.getLineEnd(i) >= textLayoutRegion.getInt(1) ? 0 : 1; + int startIndex = Math.max(layout.getLineStart(i), textLayoutRegion.getInt(0)); + int endIndex = Math.min(layout.getLineEnd(i), textLayoutRegion.getInt(1)) - offset; + + if (startIndex > endIndex + 1) { + break; + } + + Rect regionBounds = new Rect( + (int)layout.getPrimaryHorizontal(startIndex), + bounds.top, + (int)layout.getPrimaryHorizontal(endIndex), + bounds.bottom); + + WritableMap region = Arguments.createMap(); + region.putDouble("x", regionBounds.left / dm.density); + region.putDouble("y", regionBounds.top / dm.density); + region.putDouble("width", regionBounds.width() / dm.density); + region.putDouble("height", regionBounds.height() / dm.density); + region.putString("text", text.subSequence(startIndex, endIndex).toString()); + region.putInt("region", j); + region.putInt("line", i); + regions.pushMap(region); + } + } + + WritableMap textLayoutMetrics = Arguments.createMap(); + textLayoutMetrics.putArray("lines", lines); + textLayoutMetrics.putArray("regions", regions); + + return textLayoutMetrics; } } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java index 419eadb7450a4b..d17d6f77d7cf11 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextShadowNode.java @@ -24,6 +24,8 @@ import com.facebook.react.bridge.ReactSoftExceptionLogger; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.common.mapbuffer.MapBuffer; import com.facebook.react.uimanager.NativeViewHierarchyOptimizer; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ReactShadowNode; @@ -58,6 +60,7 @@ public class ReactTextShadowNode extends ReactBaseTextShadowNode { private @Nullable Spannable mPreparedSpannableText; private boolean mShouldNotifyOnTextLayout; + private MapBuffer mTextLayoutRegions; private final YogaMeasureFunction mTextMeasureFunction = new YogaMeasureFunction() { @@ -107,15 +110,14 @@ public long measure( if (mShouldNotifyOnTextLayout) { ThemedReactContext themedReactContext = getThemedContext(); - WritableArray lines = - FontMetricsUtil.getFontMetrics( - text, layout, sTextPaintInstance, themedReactContext); - WritableMap event = Arguments.createMap(); - event.putArray("lines", lines); + WritableMap textLayoutMetrics = + FontMetricsUtil.getFontMetrics( + text, layout, sTextPaintInstance, themedReactContext, mTextLayoutRegions); + if (themedReactContext.hasActiveReactInstance()) { themedReactContext .getJSModule(RCTEventEmitter.class) - .receiveEvent(getReactTag(), "topTextLayout", event); + .receiveEvent(getReactTag(), "topTextLayout", textLayoutMetrics); } else { ReactSoftExceptionLogger.logSoftException( "ReactTextShadowNode", @@ -357,6 +359,11 @@ public void setShouldNotifyOnTextLayout(boolean shouldNotifyOnTextLayout) { mShouldNotifyOnTextLayout = shouldNotifyOnTextLayout; } + @ReactProp(name = "textLayoutRegions") + public void setTextLayoutRegions(ReadableArray textLayoutRegions) { + mTextLayoutRegions = (MapBuffer) textLayoutRegions; + } + @Override public Iterable calculateLayoutOnChildren() { // Run flexbox on and return the descendants which are inline views. diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java index e5dc5fb074ce9a..0aa135af2ef4f9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java @@ -30,8 +30,10 @@ import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableNativeMap; -import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.build.ReactBuildConfig; +import com.facebook.react.common.mapbuffer.MapBuffer; +import com.facebook.react.common.mapbuffer.ReadableMapBuffer; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.ViewProps; @@ -564,11 +566,12 @@ public static long measureText( return YogaMeasureOutput.make(widthInSP, heightInSP); } - public static WritableArray measureLines( + public static WritableMap measureLines( @NonNull Context context, ReadableMap attributedString, ReadableMap paragraphAttributes, - float width) { + float width, + MapBuffer textLayoutRegions) { Spannable text = getOrCreateSpannableForText(context, attributedString, null); BoringLayout.Metrics boring = BoringLayout.isBoring(text, sTextPaintInstance); @@ -592,7 +595,7 @@ public static WritableArray measureLines( includeFontPadding, textBreakStrategy, hyphenationFrequency); - return FontMetricsUtil.getFontMetrics(text, layout, sTextPaintInstance, context); + return FontMetricsUtil.getFontMetrics(text, layout, sTextPaintInstance, context, textLayoutRegions); } // TODO T31905686: This class should be private diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java index ecb7f030f2b046..759e9c1a230a70 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java @@ -27,7 +27,8 @@ import com.facebook.common.logging.FLog; import com.facebook.react.bridge.ReactNoCrashSoftException; import com.facebook.react.bridge.ReactSoftExceptionLogger; -import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.ReadableArray; +import com.facebook.react.bridge.WritableMap; import com.facebook.react.common.build.ReactBuildConfig; import com.facebook.react.common.mapbuffer.MapBuffer; import com.facebook.react.uimanager.PixelUtil; @@ -581,11 +582,12 @@ public static long measureText( return YogaMeasureOutput.make(widthInSP, heightInSP); } - public static WritableArray measureLines( + public static WritableMap measureLines( @NonNull Context context, MapBuffer attributedString, MapBuffer paragraphAttributes, - float width) { + float width, + MapBuffer textLayoutRegions) { Spannable text = getOrCreateSpannableForText(context, attributedString, null); BoringLayout.Metrics boring = BoringLayout.isBoring(text, sTextPaintInstance); @@ -610,7 +612,8 @@ public static WritableArray measureLines( includeFontPadding, textBreakStrategy, hyphenationFrequency); - return FontMetricsUtil.getFontMetrics(text, layout, sTextPaintInstance, context); + + return FontMetricsUtil.getFontMetrics(text, layout, sTextPaintInstance, context, textLayoutRegions); } // TODO T31905686: This class should be private diff --git a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h index 26379f41e98b9b..e4c895090d4557 100644 --- a/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/packages/react-native/ReactCommon/react/renderer/attributedstring/conversions.h @@ -21,7 +21,9 @@ #include #include #include +#include #include +#include #ifdef ANDROID #include @@ -828,6 +830,30 @@ inline void fromRawValue( result = HyphenationFrequency::None; } +inline void fromRawValue( + const PropsParserContext &context, + const RawValue &value, + TextLayoutRegions &result) { + react_native_expect(value.hasType>>()); + if (value.hasType>>()) { + auto regions = std::vector>{value}; + for (const auto ®ion : regions) { + if (region.size() == 2) { + std::vector chunks; + for (const auto &chunk : region) { + chunks.push_back(chunk); + } + result.push_back(chunks); + } else { + LOG(ERROR) << "Unsupported TextLayoutRegion value"; + react_native_expect(false); + } + } + } else { + LOG(ERROR) << "Unsupported TextLayoutRegions type"; + } +} + inline ParagraphAttributes convertRawProp( const PropsParserContext &context, RawProps const &rawProps, @@ -1306,6 +1332,24 @@ inline MapBuffer toMapBuffer(const AttributedString &attributedString) { return builder.build(); } +inline MapBuffer toMapBuffer(const TextLayoutRegions &textLayoutRegions) { + auto regionsBuilder = MapBufferBuilder(); + unsigned regionIdx = 0; + + for (const auto ®ion : textLayoutRegions) { + unsigned chunkIdx = 0; + auto chunksBuilder = MapBufferBuilder(); + for (const auto &chunk : region) { + chunksBuilder.putInt(chunkIdx, chunk); + ++chunkIdx; + } + regionsBuilder.putMapBuffer(regionIdx, chunksBuilder.build()); + ++regionIdx; + } + + return regionsBuilder.build(); +} + #endif } // namespace react diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.cpp index 3ae6e664598cd8..d25f43c8043b8f 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.cpp @@ -11,9 +11,11 @@ namespace facebook::react { static jsi::Value linesMeasurementsPayload( jsi::Runtime &runtime, - LinesMeasurements const &linesMeasurements) { + LinesMeasurements const &linesMeasurements, + RegionsMeasurements const ®ionsMeasurements) { auto payload = jsi::Object(runtime); auto lines = jsi::Array(runtime, linesMeasurements.size()); + auto regions = jsi::Array(runtime, regionsMeasurements.size()); for (size_t i = 0; i < linesMeasurements.size(); ++i) { auto const &lineMeasurement = linesMeasurements[i]; @@ -29,24 +31,40 @@ static jsi::Value linesMeasurementsPayload( jsiLine.setProperty(runtime, "xHeight", lineMeasurement.xHeight); lines.setValueAtIndex(runtime, i, jsiLine); } - + + for (size_t i = 0; i < regionsMeasurements.size(); ++i) { + auto const ®ionMeasurement = regionsMeasurements[i]; + auto jsiRegion = jsi::Object(runtime); + jsiRegion.setProperty(runtime, "text", regionMeasurement.text); + jsiRegion.setProperty(runtime, "x", regionMeasurement.frame.origin.x); + jsiRegion.setProperty(runtime, "y", regionMeasurement.frame.origin.y); + jsiRegion.setProperty(runtime, "width", regionMeasurement.frame.size.width); + jsiRegion.setProperty(runtime, "height", regionMeasurement.frame.size.height); + jsiRegion.setProperty(runtime, "region", regionMeasurement.region); + jsiRegion.setProperty(runtime, "line", regionMeasurement.line); + regions.setValueAtIndex(runtime, i, jsiRegion); + } + payload.setProperty(runtime, "lines", lines); + payload.setProperty(runtime, "regions", regions); return payload; } void ParagraphEventEmitter::onTextLayout( - LinesMeasurements const &linesMeasurements) const { + LinesMeasurements const &linesMeasurements, + RegionsMeasurements const ®ionsMeasurements) const { { std::lock_guard guard(linesMeasurementsMutex_); if (linesMeasurementsMetrics_ == linesMeasurements) { return; } linesMeasurementsMetrics_ = linesMeasurements; + regionsMeasurementsMetrics_ = regionsMeasurements; } - dispatchEvent("textLayout", [linesMeasurements](jsi::Runtime &runtime) { - return linesMeasurementsPayload(runtime, linesMeasurements); + dispatchEvent("textLayout", [linesMeasurements, regionsMeasurements](jsi::Runtime &runtime) { + return linesMeasurementsPayload(runtime, linesMeasurements, regionsMeasurements); }); } diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.h b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.h index c7e306d75edcd9..c9a044f4da3d95 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphEventEmitter.h @@ -17,11 +17,12 @@ class ParagraphEventEmitter : public ViewEventEmitter { public: using ViewEventEmitter::ViewEventEmitter; - void onTextLayout(LinesMeasurements const &linesMeasurements) const; + void onTextLayout(LinesMeasurements const &linesMeasurements, RegionsMeasurements const ®ionsMeasurements) const; private: mutable std::mutex linesMeasurementsMutex_; mutable LinesMeasurements linesMeasurementsMetrics_; + mutable RegionsMeasurements regionsMeasurementsMetrics_; }; } // namespace react diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp index 1f4cbc4a4974ee..5625731d9d618a 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.cpp @@ -60,12 +60,13 @@ TextMeasurement ParagraphLayoutManager::measure( } } -LinesMeasurements ParagraphLayoutManager::measureLines( +TextLayoutMeasurements ParagraphLayoutManager::measureLines( AttributedString const &attributedString, ParagraphAttributes const ¶graphAttributes, - Size size) const { + Size size, + TextLayoutRegions textLayoutRegions) const { return textLayoutManager_->measureLines( - attributedString, paragraphAttributes, size); + attributedString, paragraphAttributes, size, textLayoutRegions); } void ParagraphLayoutManager::setTextLayoutManager( diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h index d262db62b8c9ee..ce72c9c1372bfc 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphLayoutManager.h @@ -28,10 +28,11 @@ class ParagraphLayoutManager { ParagraphAttributes const ¶graphAttributes, LayoutConstraints layoutConstraints) const; - LinesMeasurements measureLines( + TextLayoutMeasurements measureLines( AttributedString const &attributedString, ParagraphAttributes const ¶graphAttributes, - Size size) const; + Size size, + TextLayoutRegions textLayoutRegions) const; void setTextLayoutManager( std::shared_ptr textLayoutManager) const; diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.cpp index 242567dc4b6e63..734815ab4a70a3 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.cpp @@ -39,6 +39,14 @@ ParagraphProps::ParagraphProps( "selectable", sourceProps.isSelectable, false)), + textLayoutRegions( + CoreFeatures::enablePropIteratorSetter ? sourceProps.textLayoutRegions + : convertRawProp( + context, + rawProps, + "textLayoutRegions", + sourceProps.textLayoutRegions, + {})), onTextLayout( CoreFeatures::enablePropIteratorSetter ? sourceProps.onTextLayout : convertRawProp( @@ -123,6 +131,7 @@ void ParagraphProps::setProp( switch (hash) { RAW_SET_PROP_SWITCH_CASE_BASIC(isSelectable); RAW_SET_PROP_SWITCH_CASE_BASIC(onTextLayout); + RAW_SET_PROP_SWITCH_CASE_BASIC(textLayoutRegions); } /* diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.h b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.h index 86a9224c3e193f..b32ad28f61a169 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphProps.h @@ -15,6 +15,7 @@ #include #include #include +#include namespace facebook { namespace react { @@ -51,6 +52,11 @@ class ParagraphProps : public ViewProps, public BaseTextProps { */ bool isSelectable{}; + /* + * Defines text layout tracking strategy. + */ + TextLayoutRegions textLayoutRegions{}; + bool onTextLayout{}; #pragma mark - DebugStringConvertible diff --git a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp index 17cb7dd34ad1c1..42d4e344f499b1 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/text/ParagraphShadowNode.cpp @@ -158,13 +158,15 @@ void ParagraphShadowNode::layout(LayoutContext layoutContext) { auto measurement = getStateData().paragraphLayoutManager.measure( content.attributedString, content.paragraphAttributes, layoutConstraints); - + if (getConcreteProps().onTextLayout) { - auto linesMeasurements = getStateData().paragraphLayoutManager.measureLines( + auto textLayoutMeasurements = getStateData().paragraphLayoutManager.measureLines( content.attributedString, content.paragraphAttributes, - measurement.size); - getConcreteEventEmitter().onTextLayout(linesMeasurements); + measurement.size, + getConcreteProps().textLayoutRegions); + + getConcreteEventEmitter().onTextLayout(textLayoutMeasurements.linesMeasurements, textLayoutMeasurements.regionsMeasurements); } if (content.attachments.empty()) { diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.cpp b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.cpp index 03558aad542270..f62f692c0375cc 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.cpp +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.cpp @@ -65,4 +65,33 @@ bool LineMeasurement::operator==(LineMeasurement const &rhs) const { rhs.xHeight); } +RegionMeasurement::RegionMeasurement( + std::string text, + Rect frame, + int region, + int line) + : text(std::move(text)), + frame(frame), + region(region), + line(line) {} + +RegionMeasurement::RegionMeasurement(folly::dynamic const &data) + : text(data.getDefault("text", "").getString()), + frame(rectFromDynamic(data)), + region(data.getDefault("region", 0).getInt()), + line(data.getDefault("line", 0).getInt()){} + +bool RegionMeasurement::operator==(RegionMeasurement const &rhs) const { + return std::tie( + this->text, + this->frame, + this->region, + this->line) == + std::tie( + rhs.text, + rhs.frame, + rhs.region, + rhs.line); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h index 6eb4cdcf1bf59c..36e64c51958a30 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/TextMeasureCache.h @@ -38,7 +38,33 @@ struct LineMeasurement { bool operator==(LineMeasurement const &rhs) const; }; +struct RegionMeasurement { + std::string text; + Rect frame; + int region; + int line; + + RegionMeasurement( + std::string text, + Rect frame, + int region, + int line + ); + + RegionMeasurement(folly::dynamic const &data); + + bool operator==(RegionMeasurement const &rhs) const; +}; + using LinesMeasurements = std::vector; +using RegionsMeasurements = std::vector; + +using TextLayoutRegions = std::vector>; + +struct TextLayoutMeasurements { + LinesMeasurements linesMeasurements; + RegionsMeasurements regionsMeasurements; +}; /* * Describes a result of text measuring. diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp index e39f6712784f26..04e31a51a7eb12 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp @@ -226,45 +226,61 @@ TextMeasurement TextLayoutManager::measureCachedSpannableById( return TextMeasurement{size, attachments}; } -LinesMeasurements TextLayoutManager::measureLines( +TextLayoutMeasurements TextLayoutManager::measureLines( AttributedString const &attributedString, ParagraphAttributes const ¶graphAttributes, - Size size) const { + Size size, + TextLayoutRegions textLayoutRegions) const { const jni::global_ref &fabricUIManager = contextContainer_->at>("FabricUIManager"); static auto measureLines = jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") - ->getMethodgetMethod("measureLinesMapBuffer"); + jfloat, + JReadableMapBuffer::javaobject)>("measureLinesMapBuffer"); auto attributedStringMB = JReadableMapBuffer::createWithContents(toMapBuffer(attributedString)); auto paragraphAttributesMB = JReadableMapBuffer::createWithContents(toMapBuffer(paragraphAttributes)); + auto textLayoutRegionsMB = + JReadableMapBuffer::createWithContents(toMapBuffer(textLayoutRegions)); - auto array = measureLines( + auto object = measureLines( fabricUIManager, attributedStringMB.get(), paragraphAttributesMB.get(), size.width, - size.height); + size.height, + textLayoutRegionsMB.get()); - auto dynamicArray = cthis(array)->consume(); + auto dynamicObject = cthis(object)->consume(); LinesMeasurements lineMeasurements; - lineMeasurements.reserve(dynamicArray.size()); + lineMeasurements.reserve(dynamicObject.getDefault("lines").size()); + RegionsMeasurements regionsMeasurements; + regionsMeasurements.reserve(dynamicObject.getDefault("regions").size()); - for (auto const &data : dynamicArray) { + for (auto const &data : dynamicObject.getDefault("lines")) { lineMeasurements.push_back(LineMeasurement(data)); } + for (auto const &data : dynamicObject.getDefault("regions")) { + regionsMeasurements.push_back(RegionMeasurement(data)); + } + // Explicitly release smart pointers to free up space faster in JNI tables attributedStringMB.reset(); paragraphAttributesMB.reset(); - return lineMeasurements; + TextLayoutMeasurements textLayoutMeasurements = { + lineMeasurements, + regionsMeasurements, + }; + + return textLayoutMeasurements; } TextMeasurement TextLayoutManager::doMeasure( diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h index 0f5e070b942422..ce0b330f6d7e01 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h @@ -67,10 +67,11 @@ class TextLayoutManager { * Measures lines of `attributedString` using native text rendering * infrastructure. */ - LinesMeasurements measureLines( + TextLayoutMeasurements measureLines( AttributedString const &attributedString, ParagraphAttributes const ¶graphAttributes, - Size size) const; + Size size, + TextLayoutRegions textLayoutRegions) const; /* * Returns an opaque pointer to platform-specific TextLayoutManager. @@ -89,10 +90,11 @@ class TextLayoutManager { ParagraphAttributes const ¶graphAttributes, LayoutConstraints layoutConstraints) const; - LinesMeasurements measureLinesMapBuffer( + TextLayoutMeasurements measureLinesMapBuffer( AttributedString const &attributedString, ParagraphAttributes const ¶graphAttributes, - Size size) const; + Size size, + TextLayoutRegions textLayoutRegions) const; void *self_{}; ContextContainer::Shared contextContainer_; diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.cpp b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.cpp index 1284f3edfdd91a..5d04944ee056b8 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.cpp @@ -29,10 +29,11 @@ TextMeasurement TextLayoutManager::measure( return TextMeasurement{{0, 0}, attachments}; } -LinesMeasurements TextLayoutManager::measureLines( +TextLayoutMeasurements TextLayoutManager::measureLines( AttributedString attributedString, ParagraphAttributes paragraphAttributes, - Size size) const { + Size size, + TextLayoutRegions textLayoutRegions) const { return {}; }; diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.h index cb2b9ebcf0a80c..db44f4cbf40464 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/cxx/TextLayoutManager.h @@ -43,10 +43,11 @@ class TextLayoutManager { * Measures lines of `attributedString` using native text rendering * infrastructure. */ - LinesMeasurements measureLines( + TextLayoutMeasurements measureLines( AttributedString attributedString, ParagraphAttributes paragraphAttributes, - Size size) const; + Size size, + TextLayoutRegions textLayoutRegions) const; /* * Returns an opaque pointer to platform-specific TextLayoutManager. diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h index b0d906ca10cea8..dbef4d4815d084 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.h @@ -45,10 +45,10 @@ using RCTTextLayoutFragmentEnumerationBlock = frame:(CGRect)frame textStorage:(NSTextStorage *_Nullable)textStorage; -- (facebook::react::LinesMeasurements)getLinesForAttributedString:(facebook::react::AttributedString)attributedString - paragraphAttributes: - (facebook::react::ParagraphAttributes)paragraphAttributes - size:(CGSize)size; +- (facebook::react::TextLayoutMeasurements)getLinesForAttributedString:(facebook::react::AttributedString)attributedString + paragraphAttributes:(facebook::react::ParagraphAttributes)paragraphAttributes + size:(CGSize)size + textLayoutRegions:(NSArray*)textLayoutRegions; - (facebook::react::SharedEventEmitter) getEventEmitterWithAttributeString:(facebook::react::AttributedString)attributedString diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm index fc83b1fbb21f08..5e392bfc9ef978 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/RCTTextLayoutManager.mm @@ -120,9 +120,10 @@ - (void)drawAttributedString:(AttributedString)attributedString #endif } -- (LinesMeasurements)getLinesForAttributedString:(AttributedString)attributedString - paragraphAttributes:(ParagraphAttributes)paragraphAttributes - size:(CGSize)size +- (TextLayoutMeasurements)getLinesForAttributedString:(AttributedString)attributedString + paragraphAttributes:(ParagraphAttributes)paragraphAttributes + size:(CGSize)size + textLayoutRegions:(NSArray*)textLayoutRegions { NSTextStorage *textStorage = [self textStorageForAttributesString:attributedString paragraphAttributes:paragraphAttributes @@ -132,8 +133,11 @@ - (LinesMeasurements)getLinesForAttributedString:(AttributedString)attributedStr NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + __block int currentLine = 0; std::vector paragraphLines{}; + std::vector regionsMeasurements{}; auto blockParagraphLines = ¶graphLines; + auto blockRegionsMeasurements = ®ionsMeasurements; [layoutManager enumerateLineFragmentsForGlyphRange:glyphRange usingBlock:^( @@ -144,24 +148,56 @@ - (LinesMeasurements)getLinesForAttributedString:(AttributedString)attributedStr BOOL *_Nonnull stop) { NSRange range = [layoutManager characterRangeForGlyphRange:lineGlyphRange actualGlyphRange:nil]; - NSString *renderedString = [textStorage.string substringWithRange:range]; + NSString *renderedLineString = [textStorage.string substringWithRange:range]; UIFont *font = [[textStorage attributedSubstringFromRange:range] attribute:NSFontAttributeName atIndex:0 effectiveRange:nil]; - auto rect = facebook::react::Rect{ + auto lineRect = facebook::react::Rect{ facebook::react::Point{usedRect.origin.x, usedRect.origin.y}, facebook::react::Size{usedRect.size.width, usedRect.size.height}}; + + [textLayoutRegions enumerateObjectsUsingBlock:^(id _Nonnull textLayoutRegion, NSUInteger regionIdx, BOOL * _Nonnull stop) { + auto from = [[textLayoutRegion firstObject] integerValue]; + auto to = [[textLayoutRegion lastObject] integerValue] - from; + NSRange lineRange = NSIntersectionRange(lineGlyphRange, NSMakeRange(from, to)); + + if (lineRange.location == 0 && lineRange.length == 0) { + return; + } + + [layoutManager enumerateEnclosingRectsForGlyphRange:lineRange + withinSelectedGlyphRange:lineRange + inTextContainer:usedTextContainer + usingBlock:^(CGRect enclosingRect, __unused BOOL *anotherStop) { + NSString *renderedRegionString = [textStorage.string substringWithRange:lineRange]; + auto regionRect = facebook::react::Rect{ + facebook::react::Point{enclosingRect.origin.x, enclosingRect.origin.y}, + facebook::react::Size{enclosingRect.size.width, enclosingRect.size.height}}; + auto region = RegionMeasurement{ + std::string([renderedRegionString UTF8String]), + regionRect, + (int)regionIdx, + currentLine + }; + blockRegionsMeasurements->push_back(region); + }]; + }]; + auto line = LineMeasurement{ - std::string([renderedString UTF8String]), - rect, + std::string([renderedLineString UTF8String]), + lineRect, -font.descender, font.capHeight, font.ascender, font.xHeight}; blockParagraphLines->push_back(line); + currentLine += 1; }]; - return paragraphLines; + + TextLayoutMeasurements textLayoutMeasurements = {paragraphLines, regionsMeasurements}; + + return textLayoutMeasurements; } - (NSTextStorage *)textStorageForAttributesString:(AttributedString)attributedString diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.h b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.h index 095fae58850b8b..16a011175bc64c 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.h +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.h @@ -40,10 +40,11 @@ class TextLayoutManager { * Measures lines of `attributedString` using native text rendering * infrastructure. */ - LinesMeasurements measureLines( + TextLayoutMeasurements measureLines( AttributedString attributedString, ParagraphAttributes paragraphAttributes, - Size size) const; + Size size, + TextLayoutRegions textLayoutRegions) const; std::shared_ptr getHostTextStorage( AttributedString attributedString, diff --git a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm index 4f2e8719565345..d3b335316bf3a8 100644 --- a/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm +++ b/packages/react-native/ReactCommon/react/renderer/textlayoutmanager/platform/ios/react/renderer/textlayoutmanager/TextLayoutManager.mm @@ -105,15 +105,27 @@ return measurement; } -LinesMeasurements TextLayoutManager::measureLines( +TextLayoutMeasurements TextLayoutManager::measureLines( AttributedString attributedString, ParagraphAttributes paragraphAttributes, - Size size) const + Size size, + TextLayoutRegions textLayoutRegions) const { + id regions = [NSMutableArray new]; + + for (auto const &textLayoutRegion : textLayoutRegions) { + id region = [NSMutableArray new]; + for (auto const &chunk : textLayoutRegion) { + [region addObject:[NSNumber numberWithInteger:chunk]]; + } + [regions addObject:region]; + } + RCTTextLayoutManager *textLayoutManager = (RCTTextLayoutManager *)unwrapManagedObject(self_); return [textLayoutManager getLinesForAttributedString:attributedString paragraphAttributes:paragraphAttributes - size:{size.width, size.height}]; + size:{size.width, size.height} + textLayoutRegions:regions]; } } // namespace react diff --git a/packages/rn-tester/js/examples/Layout/LayoutEventsExample.js b/packages/rn-tester/js/examples/Layout/LayoutEventsExample.js index 016b0f6193ef9d..250e65cea96980 100644 --- a/packages/rn-tester/js/examples/Layout/LayoutEventsExample.js +++ b/packages/rn-tester/js/examples/Layout/LayoutEventsExample.js @@ -18,8 +18,11 @@ const { StyleSheet, Text, View, + ScrollView, } = require('react-native'); +import type {TextLayoutEvent} from 'react-native/Libraries/Types/CoreEventTypes'; + import type { ViewLayout, ViewLayoutEvent, @@ -31,6 +34,7 @@ type State = { extraText?: string, imageLayout?: ViewLayout, textLayout?: ViewLayout, + textLayoutRegions?: TextLayoutEvent['nativeEvent']['regions'], viewLayout?: ViewLayout, viewStyle: {|margin: number|}, ... @@ -76,6 +80,11 @@ class LayoutEventExample extends React.Component { this.setState({textLayout: e.nativeEvent.layout}); }; + onTextRegionsLayout = (e: TextLayoutEvent) => { + console.log('received text layout regions event\n', e.nativeEvent); + this.setState({textLayoutRegions: e.nativeEvent.regions}); + }; + onImageLayout = (e: ViewLayoutEvent) => { console.log('received image layout event\n', e.nativeEvent); this.setState({imageLayout: e.nativeEvent.layout}); @@ -85,8 +94,10 @@ class LayoutEventExample extends React.Component { const viewStyle = [styles.view, this.state.viewStyle]; const textLayout = this.state.textLayout || {width: '?', height: '?'}; const imageLayout = this.state.imageLayout || {x: '?', y: '?'}; + const textRegions = this.state.textLayoutRegions || []; + return ( - + layout events are called on mount and whenever layout is recalculated. Note that the layout event will typically be received{' '} @@ -124,7 +135,43 @@ class LayoutEventExample extends React.Component { Image x/y: {imageLayout.x}/{imageLayout.y} - + + + + TextLayout Regions:{' '} + { + /* $FlowFixMe[incompatible-type] (>=0.95.0 site=react_native_fb) + * This comment suppresses an error found when Flow v0.95 was + * deployed. To see the error, delete this comment and run Flow. + */ + // $FlowFixMe[unsafe-addition] + JSON.stringify(textRegions, null, ' ') + '\n\n' + } + + + {textRegions.map((region, index) => { + const regionStyle = { + width: region.width, + height: region.height, + top: region.y, + left: region.x, + }; + return ( + + ); + })} + + Lorem Ipsum is simply dummy text of the printing and typesetting + industry. Lorem Ipsum has been the industry's standard dummy text + ever since the 1500s, when an unknown printer took a galley of + type and scrambled it to make a type specimen book + + + + ); } } @@ -140,6 +187,7 @@ const styles = StyleSheet.create({ alignSelf: 'flex-start', borderColor: 'rgba(0, 0, 255, 0.2)', borderWidth: 0.5, + borderRadius: 4, }, image: { width: 50, @@ -153,6 +201,10 @@ const styles = StyleSheet.create({ italicText: { fontStyle: 'italic', }, + highlighted: { + backgroundColor: 'red', + position: 'absolute', + }, }); exports.title = 'Layout Events';