Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add substring measurements for Text #37397

Closed
wants to merge 19 commits into from
Closed
Show file tree
Hide file tree
Changes from 17 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 @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const textViewConfig = {
minimumFontScale: true,
textBreakStrategy: true,
onTextLayout: true,
textLayoutRegions: true,
onInlineViewLayout: true,
dataDetectorType: true,
android_hyphenationFrequency: true,
Expand Down
5 changes: 5 additions & 0 deletions packages/react-native/Libraries/Text/TextProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,11 @@ export type TextProps = $ReadOnly<{|
onMoveShouldSetResponder?: ?() => boolean,
onTextLayout?: ?(event: TextLayoutEvent) => mixed,

/**
* Regions for text layout tracking
*/
textLayoutRegions?: ?$ReadOnlyArray<$ReadOnlyArray<number>>,

/**
* Defines how far your touch may move off of the button, before
* deactivating the button.
Expand Down
11 changes: 11 additions & 0 deletions packages/react-native/Libraries/Types/CoreEventTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions packages/react-native/Libraries/Types/CoreEventTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -81,6 +91,7 @@ export type LayoutEvent = SyntheticEvent<
export type TextLayoutEvent = SyntheticEvent<
$ReadOnly<{|
lines: Array<TextLayout>,
regions: Array<TextRegion>,
|}>,
>;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,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;
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,22 @@
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 {

private static final String CAP_HEIGHT_MEASUREMENT_TEXT = "T";
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
Expand All @@ -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);
Expand All @@ -56,8 +60,41 @@ public static WritableArray getFontMetrics(
line.putDouble("xHeight", xHeight);
line.putString(
"text", text.subSequence(layout.getLineStart(i), layout.getLineEnd(i)).toString());

for (int j = 0; j < textLayoutRegions.getCount(); j++) {
MapBuffer textLayoutRegion = textLayoutRegions.getMapBuffer(j);

int startIndex = Math.max(layout.getLineStart(i), textLayoutRegion.getInt(0));
int endIndex = Math.min(layout.getLineEnd(i), textLayoutRegion.getInt(1));

if (startIndex > endIndex) {
break;
}

Rect regionBounds = new Rect(
(int)layout.getPrimaryHorizontal(startIndex),
bounds.top,
(int)layout.getPrimaryHorizontal(endIndex - 1),
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);
}

lines.pushMap(line);
}
return lines;

WritableMap textLayoutMetrics = Arguments.createMap();
textLayoutMetrics.putArray("lines", lines);
textLayoutMetrics.putArray("regions", regions);

return textLayoutMetrics;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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<? extends ReactShadowNode> calculateLayoutOnChildren() {
// Run flexbox on and return the descendants which are inline views.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -525,11 +527,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);

Expand All @@ -553,7 +556,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,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.common.mapbuffer.ReadableMapBuffer;
Expand Down Expand Up @@ -551,11 +552,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);
Expand All @@ -580,7 +582,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
#include <react/renderer/core/conversions.h>
#include <react/renderer/core/graphicsConversions.h>
#include <react/renderer/core/propsConversions.h>
#include <react/renderer/textlayoutmanager/TextMeasureCache.h>
#include <cmath>
#include <vector>

#ifdef ANDROID
#include <react/renderer/mapbuffer/MapBuffer.h>
Expand Down Expand Up @@ -827,6 +829,30 @@ inline void fromRawValue(
result = HyphenationFrequency::None;
}

inline void fromRawValue(
const PropsParserContext &context,
const RawValue &value,
TextLayoutRegions &result) {
react_native_expect(value.hasType<std::vector<std::vector<int>>>());
if (value.hasType<std::vector<std::vector<int>>>()) {
auto regions = std::vector<std::vector<int>>{value};
for (const auto &region : regions) {
if (region.size() == 2) {
std::vector<int> 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,
Expand Down Expand Up @@ -1303,6 +1329,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 &region : 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 facebook::react
Loading