diff --git a/ReactAndroid/Android-prebuilt.mk b/ReactAndroid/Android-prebuilt.mk index 8cb0ea16e1baa1..fa41a5594e0ba4 100644 --- a/ReactAndroid/Android-prebuilt.mk +++ b/ReactAndroid/Android-prebuilt.mk @@ -140,6 +140,14 @@ LOCAL_EXPORT_C_INCLUDES := \ $(REACT_COMMON_DIR)/react/renderer/mounting include $(PREBUILT_SHARED_LIBRARY) +# react_render_mapbuffer +include $(CLEAR_VARS) +LOCAL_MODULE := react_render_mapbuffer +LOCAL_SRC_FILES := $(REACT_NDK_EXPORT_DIR)/$(TARGET_ARCH_ABI)/libreact_render_mapbuffer.so +LOCAL_EXPORT_C_INCLUDES := \ + $(REACT_COMMON_DIR)/react/renderer/mapbuffer +include $(PREBUILT_SHARED_LIBRARY) + # rrc_view include $(CLEAR_VARS) LOCAL_MODULE := rrc_view diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/BUCK b/ReactAndroid/src/main/java/com/facebook/react/common/BUCK index 596d953a230158..02246b533959ab 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/common/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/common/BUCK @@ -2,6 +2,7 @@ load("//tools/build_defs/oss:rn_defs.bzl", "react_native_dep", "rn_android_build SUB_PROJECTS = [ "network/**/*", + "mapbuffer/**/*", ] rn_android_library( diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/BUCK b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/BUCK new file mode 100644 index 00000000000000..58366da207e166 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/BUCK @@ -0,0 +1,26 @@ +load("//tools/build_defs/oss:rn_defs.bzl", "FBJNI_TARGET", "react_native_dep", "react_native_target", "rn_android_library") + +rn_android_library( + name = "mapbuffer", + srcs = glob([ + "*.java", + ]), + autoglob = False, + is_androidx = True, + labels = ["supermodule:xplat/default/public.react_native.infra"], + provided_deps = [], + required_for_source_only_abi = True, + visibility = [ + "PUBLIC", + ], + deps = [ + FBJNI_TARGET, + react_native_dep("libraries/soloader/java/com/facebook/soloader:soloader"), + react_native_target("java/com/facebook/react/common/mapbuffer/jni:jni"), + react_native_dep("libraries/fbjni:java"), + react_native_dep("third-party/android/androidx:annotation"), + react_native_dep("third-party/java/infer-annotations:infer-annotations"), + react_native_target("java/com/facebook/react/common:common"), + ], + exported_deps = [], +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.java b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.java new file mode 100644 index 00000000000000..e70c3c090cc7e9 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/ReadableMapBuffer.java @@ -0,0 +1,348 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.common.mapbuffer; + +import androidx.annotation.Nullable; +import com.facebook.jni.HybridData; +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.soloader.SoLoader; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Iterator; + +/** + * TODO T83483191: add documentation. + * + *

NOTE: {@link ReadableMapBuffer} is NOT thread safe. + */ +public class ReadableMapBuffer implements Iterable { + + static { + SoLoader.loadLibrary("mapbufferjni"); + } + + // Value used to verify if the data is serialized with LittleEndian order. + private static final int ALIGNMENT = 0xFE; + + // 6 bytes = 2 (alignment) + 2 (count) + 2 (size) + private static final int HEADER_SIZE = 6; + + // key size = 2 bytes + private static final int KEY_SIZE = 2; + + // 10 bytes = 2 bytes key + 8 bytes value + private static final int BUCKET_SIZE = 10; + + private static final int INT_SIZE = 4; + + // TODO T83483191: consider moving short to INTs, we are doing extra cast operations just because + // of short java operates with int + private static final int SHORT_SIZE = 2; + + private static final short SHORT_ONE = (short) 1; + + @Nullable ByteBuffer mBuffer = null; + + // Size of the Serialized Data + @SuppressWarnings("unused") + private short mSizeOfData = 0; + + // Amount of items serialized on the ByteBuffer + @SuppressWarnings("unused") + private short mCount = 0; + + private ReadableMapBuffer(HybridData hybridData) { + mHybridData = hybridData; + } + + private ReadableMapBuffer(ByteBuffer buffer) { + mBuffer = buffer; + readHeader(); + } + + private native ByteBuffer importByteBufferAllocateDirect(); + + private native ByteBuffer importByteBuffer(); + + @SuppressWarnings("unused") + @DoNotStrip + @Nullable + private HybridData mHybridData; + + @Override + protected void finalize() throws Throwable { + super.finalize(); + if (mHybridData != null) { + mHybridData.resetNative(); + } + } + + private int getKeyOffsetForBucketIndex(int bucketIndex) { + return HEADER_SIZE + BUCKET_SIZE * bucketIndex; + } + + private int getValueOffsetForKey(short key) { + importByteBufferAndReadHeader(); + int bucketIndex = getBucketIndexForKey(key); + if (bucketIndex == -1) { + // TODO T83483191: Add tests + throw new IllegalArgumentException("Unable to find key: " + key); + } + assertKeyExists(key, bucketIndex); + return getKeyOffsetForBucketIndex(bucketIndex) + KEY_SIZE; + } + + // returns the relative offset of the first byte of dynamic data + private int getOffsetForDynamicData() { + // TODO T83483191: check if there's dynamic data? + return getKeyOffsetForBucketIndex(mCount); + } + + /** + * @param key Key to search for + * @return the "bucket index" for a key or -1 if not found. It uses a binary search algorithm + * (log(n)) + */ + private int getBucketIndexForKey(short key) { + short lo = 0; + short hi = (short) (getCount() - SHORT_ONE); + while (lo <= hi) { + final short mid = (short) ((lo + hi) >>> SHORT_ONE); + final short midVal = readKey(getKeyOffsetForBucketIndex(mid)); + if (midVal < key) { + lo = (short) (mid + SHORT_ONE); + } else if (midVal > key) { + hi = (short) (mid - SHORT_ONE); + } else { + return mid; + } + } + return -1; + } + + private short readKey(int position) { + return mBuffer.getShort(position); + } + + private double readDoubleValue(int bufferPosition) { + return mBuffer.getDouble(bufferPosition); + } + + private int readIntValue(int bufferPosition) { + return mBuffer.getInt(bufferPosition); + } + + private boolean readBooleanValue(int bufferPosition) { + return readIntValue(bufferPosition) == 1; + } + + private String readStringValue(int bufferPosition) { + int offset = getOffsetForDynamicData() + mBuffer.getInt(bufferPosition); + + int sizeOfString = mBuffer.getInt(offset); + byte[] result = new byte[sizeOfString]; + + int stringOffset = offset + INT_SIZE; + + mBuffer.position(stringOffset); + mBuffer.get(result, 0, sizeOfString); + + return new String(result); + } + + private ReadableMapBuffer readMapBufferValue(int position) { + int offset = getOffsetForDynamicData() + mBuffer.getInt(position); + + int sizeMapBuffer = mBuffer.getShort(offset); + byte[] buffer = new byte[sizeMapBuffer]; + + int bufferOffset = offset + SHORT_SIZE; + + mBuffer.position(bufferOffset); + mBuffer.get(buffer, 0, sizeMapBuffer); + + return new ReadableMapBuffer(ByteBuffer.wrap(buffer)); + } + + private void readHeader() { + // byte order + short storedAlignment = mBuffer.getShort(); + if (storedAlignment != ALIGNMENT) { + mBuffer.order(ByteOrder.LITTLE_ENDIAN); + } + // count + mCount = mBuffer.getShort(); + // size + mSizeOfData = mBuffer.getShort(); + } + + /** + * Binary search of the key inside the mapBuffer (log(n)). + * + * @param key Key to search for + * @return true if and only if the Key received as a parameter is stored in the MapBuffer. + */ + public boolean hasKey(short key) { + // TODO T83483191: Add tests + return getBucketIndexForKey(key) != -1; + } + + /** @return amount of elements stored into the MapBuffer */ + public short getCount() { + importByteBufferAndReadHeader(); + return mCount; + } + + /** + * @param key {@link int} representing the key + * @return return the int associated to the Key received as a parameter. + */ + public int getInt(short key) { + // TODO T83483191: extract common code of "get methods" + return readIntValue(getValueOffsetForKey(key)); + } + + /** + * @param key {@link int} representing the key + * @return return the double associated to the Key received as a parameter. + */ + public double getDouble(short key) { + return readDoubleValue(getValueOffsetForKey(key)); + } + + /** + * @param key {@link int} representing the key + * @return return the int associated to the Key received as a parameter. + */ + public String getString(short key) { + return readStringValue(getValueOffsetForKey(key)); + } + + public boolean getBoolean(short key) { + return readBooleanValue(getValueOffsetForKey(key)); + } + + /** + * @param key {@link int} representing the key + * @return return the int associated to the Key received as a parameter. + */ + public ReadableMapBuffer getMapBuffer(short key) { + return readMapBufferValue(getValueOffsetForKey(key)); + } + + /** + * Import ByteBuffer from C++, read the header and move the current cursor at the start of the + * payload. + */ + private ByteBuffer importByteBufferAndReadHeader() { + if (mBuffer != null) { + return mBuffer; + } + + // mBuffer = importByteBufferAllocateDirect(); + mBuffer = importByteBuffer(); + + readHeader(); + return mBuffer; + } + + private void assertKeyExists(short key, int bucketIndex) { + short storedKey = mBuffer.getShort(getKeyOffsetForBucketIndex(bucketIndex)); + if (storedKey != key) { + throw new IllegalStateException( + "Stored key doesn't match parameter - expected: " + key + " - found: " + storedKey); + } + } + + @Override + public int hashCode() { + ByteBuffer byteBuffer = importByteBufferAndReadHeader(); + byteBuffer.rewind(); + return byteBuffer.hashCode(); + } + + @Override + public boolean equals(@Nullable Object obj) { + if (!(obj instanceof ReadableMapBuffer)) { + return false; + } + + ReadableMapBuffer other = (ReadableMapBuffer) obj; + ByteBuffer thisByteBuffer = importByteBufferAndReadHeader(); + ByteBuffer otherByteBuffer = other.importByteBufferAndReadHeader(); + if (thisByteBuffer == otherByteBuffer) { + return true; + } + thisByteBuffer.rewind(); + otherByteBuffer.rewind(); + return thisByteBuffer.equals(otherByteBuffer); + } + + /** @return an {@link Iterator} for the entries of this MapBuffer. */ + @Override + public Iterator iterator() { + return new Iterator() { + short current = 0; + short last = (short) (getCount() - SHORT_ONE); + + @Override + public boolean hasNext() { + return current <= last; + } + + @Override + public MapBufferEntry next() { + return new MapBufferEntry(getKeyOffsetForBucketIndex(current++)); + } + }; + } + + /** This class represents an Entry of the {@link ReadableMapBuffer} class. */ + public class MapBufferEntry { + private final int mBucketOffset; + + private MapBufferEntry(int position) { + mBucketOffset = position; + } + + /** @return a {@link short} that represents the key of this {@link MapBufferEntry}. */ + public short getKey() { + return readKey(mBucketOffset); + } + + /** @return the double value that is stored in this {@link MapBufferEntry}. */ + public double getDouble(double defaultValue) { + // TODO T83483191 Extend serialization of MapBuffer to add type checking + // TODO T83483191 Extend serialization of MapBuffer to return null if there's no value + // stored in this MapBufferEntry. + return readDoubleValue(mBucketOffset + KEY_SIZE); + } + + /** @return the int value that is stored in this {@link MapBufferEntry}. */ + public int getInt(int defaultValue) { + return readIntValue(mBucketOffset + KEY_SIZE); + } + + /** @return the boolean value that is stored in this {@link MapBufferEntry}. */ + public boolean getBoolean(boolean defaultValue) { + return readBooleanValue(mBucketOffset + KEY_SIZE); + } + + /** @return the String value that is stored in this {@link MapBufferEntry}. */ + public @Nullable String getString() { + return readStringValue(mBucketOffset + KEY_SIZE); + } + + /** + * @return the {@link ReadableMapBuffer} value that is stored in this {@link MapBufferEntry}. + */ + public @Nullable ReadableMapBuffer getReadableMapBuffer() { + return readMapBufferValue(mBucketOffset + KEY_SIZE); + } + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/Android.mk b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/Android.mk new file mode 100644 index 00000000000000..b74cc058fcf143 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/Android.mk @@ -0,0 +1,39 @@ +# Copyright (c) Facebook, Inc. and its affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +LOCAL_PATH := $(call my-dir) + +include $(CLEAR_VARS) + +LOCAL_MODULE := mapbufferjni + +LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/react/common/mapbuffer/*.cpp) + +LOCAL_SHARED_LIBRARIES := libreactconfig libyoga libglog libfb libfbjni libglog_init libfolly_json libfolly_futures libreact_utils libreact_render_mapbuffer libreact_debug + +LOCAL_STATIC_LIBRARIES := + +LOCAL_C_INCLUDES := $(LOCAL_PATH)/ + +LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/ + +LOCAL_CFLAGS := \ + -DLOG_TAG=\"Fabric\" + +LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall + +include $(BUILD_SHARED_LIBRARY) + +$(call import-module,fbgloginit) +$(call import-module,folly) +$(call import-module,fb) +$(call import-module,fbjni) +$(call import-module,yogajni) +$(call import-module,glog) + +$(call import-module,react/utils) +$(call import-module,react/debug) +$(call import-module,react/config) +$(call import-module,react/renderer/mapbuffer) diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/BUCK b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/BUCK new file mode 100644 index 00000000000000..591ebda315470c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/BUCK @@ -0,0 +1,33 @@ +load("//tools/build_defs/oss:rn_defs.bzl", "ANDROID", "FBJNI_TARGET", "react_native_xplat_target", "rn_xplat_cxx_library", "subdir_glob") + +rn_xplat_cxx_library( + name = "jni", + srcs = glob(["**/*.cpp"]), + headers = glob(["**/*.h"]), + header_namespace = "", + exported_headers = subdir_glob( + [ + ("react/common/mapbuffer", "*.h"), + ], + prefix = "react/common/mapbuffer", + ), + compiler_flags = [ + "-fexceptions", + "-frtti", + "-std=c++14", + "-Wall", + ], + fbandroid_allow_jni_merging = True, + labels = ["supermodule:xplat/default/public.react_native.infra"], + platforms = (ANDROID), + preprocessor_flags = [ + "-DLOG_TAG=\"ReactNative\"", + "-DWITH_FBSYSTRACE=1", + ], + soname = "libmapbufferjni.$(ext)", + visibility = ["PUBLIC"], + deps = [ + react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"), + FBJNI_TARGET, + ], +) diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/OnLoad.cpp b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/OnLoad.cpp new file mode 100644 index 00000000000000..76e0aa1a2c877b --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/OnLoad.cpp @@ -0,0 +1,15 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +#include "ReadableMapBuffer.h" + +JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) { + return facebook::jni::initialize( + vm, [] { facebook::react::ReadableMapBuffer::registerNatives(); }); +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/ReadableMapBuffer.cpp b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/ReadableMapBuffer.cpp new file mode 100644 index 00000000000000..8f7cac20b28179 --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/ReadableMapBuffer.cpp @@ -0,0 +1,62 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "ReadableMapBuffer.h" + +namespace facebook { +namespace react { + +void ReadableMapBuffer::registerNatives() { + registerHybrid({ + makeNativeMethod("importByteBuffer", ReadableMapBuffer::importByteBuffer), + makeNativeMethod( + "importByteBufferAllocateDirect", + ReadableMapBuffer::importByteBufferAllocateDirect), + }); +} + +jni::local_ref +ReadableMapBuffer::importByteBufferAllocateDirect() { + // TODO: Using this method is safer than "importByteBuffer" because ByteBuffer + // memory will be deallocated once the "Java ByteBuffer" is deallocated. Next + // steps: + // - Validate perf of this method vs importByteBuffer + // - Validate that there's no leaking of memory + return jni::JByteBuffer::allocateDirect(_serializedDataSize); +} + +jni::JByteBuffer::javaobject ReadableMapBuffer::importByteBuffer() { + // TODO: Reevaluate what's the best approach here (allocateDirect vs + // DirectByteBuffer). + // + // On this method we should: + // - Review deallocation of serializedData (we are probably leaking + // _serializedData now). + // - Consider using allocate() or allocateDirect() methods from java instead + // of newDirectByteBuffer (to simplify de/allocation) : + // https://www.internalfb.com/intern/diffusion/FBS/browsefile/master/fbandroid/libraries/fbjni/cxx/fbjni/ByteBuffer.cpp + // - Add flags to describe if the data was already 'imported' + // - Long-term: Consider creating a big ByteBuffer that can be re-used to + // transfer data of multitple Maps + return static_cast( + jni::Environment::current()->NewDirectByteBuffer( + (void *)_serializedData, _serializedDataSize)); +} + +jni::local_ref +ReadableMapBuffer::createWithContents(MapBuffer &&map) { + return newObjectCxxArgs(std::move(map)); +} + +ReadableMapBuffer::~ReadableMapBuffer() { + delete[] _serializedData; + _serializedData = nullptr; + _serializedDataSize = 0; +} + +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/ReadableMapBuffer.h b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/ReadableMapBuffer.h new file mode 100644 index 00000000000000..555851199f563c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/common/mapbuffer/jni/react/common/mapbuffer/ReadableMapBuffer.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +#include + +namespace facebook { +namespace react { + +class ReadableMapBuffer : public jni::HybridClass { + public: + static auto constexpr kJavaDescriptor = + "Lcom/facebook/react/common/mapbuffer/ReadableMapBuffer;"; + + static void registerNatives(); + + static jni::local_ref createWithContents(MapBuffer &&map); + + jni::local_ref importByteBufferAllocateDirect(); + + jni::JByteBuffer::javaobject importByteBuffer(); + + ~ReadableMapBuffer(); + + private: + uint8_t *_serializedData = nullptr; + + int _serializedDataSize = 0; + + friend HybridBase; + + explicit ReadableMapBuffer(MapBuffer &&map) { + _serializedDataSize = map.getBufferSize(); + _serializedData = new Byte[_serializedDataSize]; + map.copy(_serializedData); + } +}; + +} // namespace react +} // namespace facebook diff --git a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java index 75e7c0f16869fa..a3ddb7cff7fe02 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java +++ b/ReactAndroid/src/main/java/com/facebook/react/config/ReactFeatureFlags.java @@ -65,4 +65,7 @@ public class ReactFeatureFlags { /** Enables JS Responder in Fabric */ public static boolean enableJSResponder = false; + + /** Enables MapBuffer Serialization */ + public static boolean mapBufferSerializationEnabled = false; } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/BUCK b/ReactAndroid/src/main/java/com/facebook/react/fabric/BUCK index 12ad6392e594d6..df7dee67bd64e0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/BUCK @@ -37,6 +37,7 @@ rn_android_library( react_native_target("java/com/facebook/react/modules/core:core"), react_native_target("java/com/facebook/react/modules/i18nmanager:i18nmanager"), react_native_target("java/com/facebook/react/common:common"), + react_native_target("java/com/facebook/react/common/mapbuffer:mapbuffer"), react_native_target("java/com/facebook/react/uimanager:uimanager"), react_native_target("java/com/facebook/react/views/view:view"), react_native_target("java/com/facebook/react/views/text:text"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index abcea19b8a78cb..89ea427748dd76 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -53,6 +53,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.ReadableMapBuffer; import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.fabric.events.EventBeatManager; import com.facebook.react.fabric.events.EventEmitterWrapper; @@ -81,6 +82,7 @@ import com.facebook.react.uimanager.events.EventDispatcher; import com.facebook.react.uimanager.events.EventDispatcherImpl; import com.facebook.react.views.text.TextLayoutManager; +import com.facebook.react.views.text.TextLayoutManagerMapBuffer; import com.facebook.systrace.Systrace; import java.util.ArrayList; import java.util.Collection; @@ -418,6 +420,21 @@ private NativeArray measureLines( PixelUtil.toPixelFromDIP(width)); } + @DoNotStrip + @SuppressWarnings("unused") + private NativeArray measureLinesMapBuffer( + ReadableMapBuffer attributedString, + ReadableMapBuffer paragraphAttributes, + float width, + float height) { + return (NativeArray) + TextLayoutManagerMapBuffer.measureLines( + mReactApplicationContext, + attributedString, + paragraphAttributes, + PixelUtil.toPixelFromDIP(width)); + } + @DoNotStrip @SuppressWarnings("unused") private long measure( @@ -482,6 +499,44 @@ private long measure( attachmentsPositions); } + @DoNotStrip + @SuppressWarnings("unused") + private long measureMapBuffer( + int surfaceId, + String componentName, + ReadableMapBuffer attributedString, + ReadableMapBuffer paragraphAttributes, + float minWidth, + float maxWidth, + float minHeight, + float maxHeight, + @Nullable float[] attachmentsPositions) { + + ReactContext context; + if (surfaceId > 0) { + SurfaceMountingManager surfaceMountingManager = + mMountingManager.getSurfaceManagerEnforced(surfaceId, "measure"); + if (surfaceMountingManager.isStopped()) { + return 0; + } + context = surfaceMountingManager.getContext(); + } else { + context = mReactApplicationContext; + } + + // TODO: replace ReadableNativeMap -> ReadableMapBuffer + return mMountingManager.measureTextMapBuffer( + context, + componentName, + attributedString, + paragraphAttributes, + getYogaSize(minWidth, maxWidth), + getYogaMeasureMode(minWidth, maxWidth), + getYogaSize(minHeight, maxHeight), + getYogaMeasureMode(minHeight, maxHeight), + attachmentsPositions); + } + /** * @param surfaceId {@link int} surface ID * @param defaultTextInputPadding {@link float[]} output parameter will contain the default theme diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/StateWrapperImpl.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/StateWrapperImpl.java index 06d4459c3021fc..821dff8a5fdfb1 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/StateWrapperImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/StateWrapperImpl.java @@ -16,6 +16,7 @@ import com.facebook.react.bridge.NativeMap; import com.facebook.react.bridge.ReadableNativeMap; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.mapbuffer.ReadableMapBuffer; import com.facebook.react.uimanager.StateWrapper; /** @@ -41,6 +42,18 @@ private StateWrapperImpl() { private native ReadableNativeMap getStateDataImpl(); + private native ReadableMapBuffer getStateMapBufferDataImpl(); + + @Override + @Nullable + public ReadableMapBuffer getStatDataMapBuffer() { + if (mDestroyed) { + FLog.e(TAG, "Race between StateWrapperImpl destruction and getState"); + return null; + } + return getStateMapBufferDataImpl(); + } + @Override @Nullable public ReadableNativeMap getStateData() { diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Android.mk b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Android.mk index a20d60caf044f3..a6353d07765ba3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Android.mk +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Android.mk @@ -11,7 +11,7 @@ LOCAL_MODULE := fabricjni LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) -LOCAL_SHARED_LIBRARIES := libreactconfig librrc_slider librrc_progressbar librrc_switch librrc_modal libyoga libglog libfb libfbjni libglog_init libfolly_json libfolly_futures libreact_render_mounting libreactnativeutilsjni libreact_utils libreact_render_debug libreact_render_graphics libreact_render_core libreact_render_mapbuffer react_render_componentregistry librrc_view librrc_unimplementedview librrc_root librrc_scrollview libbetter libreact_render_attributedstring libreact_render_uimanager libreact_render_templateprocessor libreact_render_scheduler libreact_render_animations libreact_render_imagemanager libreact_render_textlayoutmanager libreact_codegen_rncore rrc_text librrc_image librrc_textinput librrc_picker libreact_debug +LOCAL_SHARED_LIBRARIES := libreactconfig librrc_slider librrc_progressbar librrc_switch librrc_modal libyoga libglog libfb libfbjni libglog_init libfolly_json libfolly_futures libreact_render_mounting libreactnativeutilsjni libreact_utils libreact_render_debug libreact_render_graphics libreact_render_core react_render_componentregistry librrc_view librrc_unimplementedview librrc_root librrc_scrollview libbetter libreact_render_attributedstring libreact_render_uimanager libreact_render_templateprocessor libreact_render_scheduler libreact_render_animations libreact_render_imagemanager libreact_render_textlayoutmanager libreact_codegen_rncore rrc_text librrc_image librrc_textinput librrc_picker libreact_debug libreact_render_mapbuffer libmapbufferjni LOCAL_STATIC_LIBRARIES := diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/BUCK b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/BUCK index 9359bfb6f77ffc..0dca065b7eb5b0 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/BUCK @@ -27,6 +27,7 @@ rn_xplat_cxx_library( soname = "libfabricjni.$(ext)", visibility = ["PUBLIC"], deps = [ + react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"), react_native_xplat_target("react/config:config"), react_native_xplat_target("react/renderer/animations:animations"), react_native_xplat_target("react/renderer/uimanager:uimanager"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp index f1ff744d17de0b..847d3f10716b9f 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/Binding.cpp @@ -508,6 +508,11 @@ void Binding::installFabricUIManager( // Keep reference to config object and cache some feature flags here reactNativeConfig_ = config; + contextContainer->insert( + "MapBufferSerializationEnabled", + reactNativeConfig_->getBool( + "react_fabric:enable_mapbuffer_serialization_android")); + disablePreallocateViews_ = reactNativeConfig_->getBool( "react_fabric:disabled_view_preallocation_android"); diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/StateWrapperImpl.cpp b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/StateWrapperImpl.cpp index 0def0aab5ff0d1..eaa9398de60b07 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/StateWrapperImpl.cpp +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/StateWrapperImpl.cpp @@ -8,6 +8,8 @@ #include "StateWrapperImpl.h" #include #include +#include +#include using namespace facebook::jni; @@ -30,6 +32,14 @@ StateWrapperImpl::getStateDataImpl() { return readableNativeMap; } +jni::local_ref +StateWrapperImpl::getStateMapBufferDataImpl() { + MapBuffer map = state_->getMapBuffer(); + auto ReadableMapBuffer = + ReadableMapBuffer::createWithContents(std::move(map)); + return ReadableMapBuffer; +} + void StateWrapperImpl::updateStateImpl(NativeMap *map) { // Get folly::dynamic from map auto dynamicMap = map->consume(); @@ -42,6 +52,9 @@ void StateWrapperImpl::registerNatives() { makeNativeMethod("initHybrid", StateWrapperImpl::initHybrid), makeNativeMethod("getStateDataImpl", StateWrapperImpl::getStateDataImpl), makeNativeMethod("updateStateImpl", StateWrapperImpl::updateStateImpl), + makeNativeMethod( + "getStateMapBufferDataImpl", + StateWrapperImpl::getStateMapBufferDataImpl), }); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/StateWrapperImpl.h b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/StateWrapperImpl.h index 21e0c4c2fb18de..9193efc2518d49 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/StateWrapperImpl.h +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/StateWrapperImpl.h @@ -8,6 +8,7 @@ #pragma once #include +#include #include #include @@ -25,6 +26,7 @@ class StateWrapperImpl : public jni::HybridClass { static void registerNatives(); + jni::local_ref getStateMapBufferDataImpl(); jni::local_ref getStateDataImpl(); void updateStateImpl(NativeMap *map); void updateStateWithFailureCallbackImpl( diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java index a45742450dc8b8..7993e76e902fbd 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/MountingManager.java @@ -9,6 +9,7 @@ import static com.facebook.infer.annotation.ThreadConfined.ANY; +import android.text.Spannable; import android.view.View; import androidx.annotation.AnyThread; import androidx.annotation.NonNull; @@ -22,6 +23,7 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.RetryableMountingLayerException; import com.facebook.react.bridge.UiThreadUtil; +import com.facebook.react.common.mapbuffer.ReadableMapBuffer; import com.facebook.react.fabric.FabricUIManager; import com.facebook.react.fabric.events.EventEmitterWrapper; import com.facebook.react.fabric.mounting.mountitems.MountItem; @@ -30,6 +32,8 @@ import com.facebook.react.uimanager.RootViewManager; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.ViewManagerRegistry; +import com.facebook.react.views.text.ReactTextViewManagerCallback; +import com.facebook.react.views.text.TextLayoutManagerMapBuffer; import com.facebook.yoga.YogaMeasureMode; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -337,6 +341,49 @@ public long measure( attachmentsPositions); } + /** + * Measure a component, given localData, props, state, and measurement information. This needs to + * remain here for now - and not in SurfaceMountingManager - because sometimes measures are made + * outside of the context of a Surface; especially from C++ before StartSurface is called. + * + * @param context + * @param componentName + * @param attributedString + * @param paragraphAttributes + * @param width + * @param widthMode + * @param height + * @param heightMode + * @param attachmentsPositions + * @return + */ + @AnyThread + public long measureTextMapBuffer( + @NonNull ReactContext context, + @NonNull String componentName, + @NonNull ReadableMapBuffer attributedString, + @NonNull ReadableMapBuffer paragraphAttributes, + float width, + @NonNull YogaMeasureMode widthMode, + float height, + @NonNull YogaMeasureMode heightMode, + @Nullable float[] attachmentsPositions) { + + return TextLayoutManagerMapBuffer.measureText( + context, + attributedString, + paragraphAttributes, + width, + widthMode, + height, + heightMode, + new ReactTextViewManagerCallback() { + @Override + public void onPostProcessSpannable(Spannable text) {} + }, + attachmentsPositions); + } + public void initializeViewManager(String componentName) { mViewManagerRegistry.get(componentName); } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BUCK b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BUCK index 72db6f8efc59e8..9979f6dfdde5d2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BUCK @@ -39,6 +39,7 @@ rn_android_library( react_native_target("java/com/facebook/react/bridge:bridge"), react_native_target("java/com/facebook/react/uimanager/jni:jni"), react_native_target("java/com/facebook/react/common:common"), + react_native_target("java/com/facebook/react/common/mapbuffer:mapbuffer"), react_native_target("java/com/facebook/react/config:config"), react_native_target("java/com/facebook/react/module/annotations:annotations"), react_native_target("java/com/facebook/react/modules/core:core"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/StateWrapper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/StateWrapper.java index 1ad52d95affb58..8bec238604f64d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/StateWrapper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/StateWrapper.java @@ -9,6 +9,7 @@ import com.facebook.react.bridge.ReadableNativeMap; import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.mapbuffer.ReadableMapBuffer; import javax.annotation.Nullable; /** @@ -17,6 +18,15 @@ * by calling updateState, which communicates state back to the C++ layer. */ public interface StateWrapper { + + /** + * Get a ReadableMapBuffer object from the C++ layer, which is a K/V map of short keys to values. + * + *

Unstable API - DO NOT USE. + */ + @Nullable + ReadableMapBuffer getStatDataMapBuffer(); + /** * Get a ReadableNativeMap object from the C++ layer, which is a K/V map of string keys to values. */ diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK b/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK index 49b14f8a7176a0..cf4fbdd897016a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/BUCK @@ -23,6 +23,7 @@ rn_android_library( react_native_dep("third-party/java/jsr-305:jsr-305"), react_native_target("java/com/facebook/react/bridge:bridge"), react_native_target("java/com/facebook/react/common:common"), + react_native_target("java/com/facebook/react/common/mapbuffer:mapbuffer"), react_native_target("java/com/facebook/react/config:config"), react_native_target("java/com/facebook/react/module/annotations:annotations"), react_native_target("java/com/facebook/react/uimanager:uimanager"), diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java index 28f575e956998a..ba871e1679fc1d 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/ReactTextViewManager.java @@ -14,6 +14,8 @@ import com.facebook.react.bridge.ReadableNativeMap; import com.facebook.react.common.MapBuilder; import com.facebook.react.common.annotations.VisibleForTesting; +import com.facebook.react.common.mapbuffer.ReadableMapBuffer; +import com.facebook.react.config.ReactFeatureFlags; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.uimanager.IViewManagerWithChildren; import com.facebook.react.uimanager.ReactStylesDiffMap; @@ -31,6 +33,12 @@ public class ReactTextViewManager extends ReactTextAnchorViewManager implements IViewManagerWithChildren { + private static final short TX_STATE_KEY_ATTRIBUTED_STRING = 0; + private static final short TX_STATE_KEY_PARAGRAPH_ATTRIBUTES = 1; + // used for text input + private static final short TX_STATE_KEY_HASH = 2; + private static final short TX_STATE_KEY_MOST_RECENT_EVENT_COUNT = 3; + @VisibleForTesting public static final String REACT_CLASS = "RCTText"; protected @Nullable ReactTextViewManagerCallback mReactTextViewManagerCallback; @@ -87,6 +95,13 @@ public Object updateState( return null; } + if (ReactFeatureFlags.mapBufferSerializationEnabled) { + ReadableMapBuffer stateMapBuffer = stateWrapper.getStatDataMapBuffer(); + if (stateMapBuffer != null) { + return getReactTextUpdate(view, props, stateMapBuffer); + } + } + ReadableNativeMap state = stateWrapper.getStateData(); if (state == null) { return null; @@ -111,6 +126,30 @@ public Object updateState( TextAttributeProps.getJustificationMode(props)); } + private Object getReactTextUpdate( + ReactTextView view, ReactStylesDiffMap props, ReadableMapBuffer state) { + + ReadableMapBuffer attributedString = state.getMapBuffer(TX_STATE_KEY_ATTRIBUTED_STRING); + ReadableMapBuffer paragraphAttributes = state.getMapBuffer(TX_STATE_KEY_PARAGRAPH_ATTRIBUTES); + Spannable spanned = + TextLayoutManagerMapBuffer.getOrCreateSpannableForText( + view.getContext(), attributedString, mReactTextViewManagerCallback); + view.setSpanned(spanned); + + int textBreakStrategy = + TextAttributeProps.getTextBreakStrategy( + paragraphAttributes.getString(TextLayoutManagerMapBuffer.PA_KEY_TEXT_BREAK_STRATEGY)); + + return new ReactTextUpdate( + spanned, + -1, // UNUSED FOR TEXT + false, // TODO add this into local Data + TextAttributeProps.getTextAlignment( + props, TextLayoutManagerMapBuffer.isRTL(attributedString)), + textBreakStrategy, + TextAttributeProps.getJustificationMode(props)); + } + @Override public @Nullable Map getExportedCustomDirectEventTypeConstants() { return MapBuilder.of( diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java index 53ae6b4e502fcf..fe926cb743c513 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextAttributeProps.java @@ -10,23 +10,52 @@ import android.graphics.Typeface; import android.os.Build; import android.text.Layout; +import android.text.TextUtils; import android.util.LayoutDirection; import android.view.Gravity; import androidx.annotation.Nullable; import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.common.mapbuffer.ReadableMapBuffer; import com.facebook.react.uimanager.PixelUtil; import com.facebook.react.uimanager.ReactAccessibilityDelegate; import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.ViewProps; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; // TODO: T63643819 refactor naming of TextAttributeProps to make explicit that this represents // TextAttributes and not TextProps. As part of this refactor extract methods that don't belong to // TextAttributeProps (e.g. TextAlign) public class TextAttributeProps { - private static final String INLINE_IMAGE_PLACEHOLDER = "I"; + // constants for Text Attributes serialization + public static final short TA_KEY_FOREGROUND_COLOR = 0; + public static final short TA_KEY_BACKGROUND_COLOR = 1; + public static final short TA_KEY_OPACITY = 2; + public static final short TA_KEY_FONT_FAMILY = 3; + public static final short TA_KEY_FONT_SIZE = 4; + public static final short TA_KEY_FONT_SIZE_MULTIPLIER = 5; + public static final short TA_KEY_FONT_WEIGHT = 6; + public static final short TA_KEY_FONT_STYLE = 7; + public static final short TA_KEY_FONT_VARIANT = 8; + public static final short TA_KEY_ALLOW_FONT_SCALING = 9; + public static final short TA_KEY_LETTER_SPACING = 10; + public static final short TA_KEY_LINE_HEIGHT = 11; + public static final short TA_KEY_ALIGNMENT = 12; + public static final short TA_KEY_BEST_WRITING_DIRECTION = 13; + public static final short TA_KEY_TEXT_DECORATION_COLOR = 14; + public static final short TA_KEY_TEXT_DECORATION_LINE = 15; + public static final short TA_KEY_TEXT_DECORATION_LINE_STYLE = 16; + public static final short TA_KEY_TEXT_DECORATION_LINE_PATTERN = 17; + public static final short TA_KEY_TEXT_SHADOW_RAIDUS = 18; + public static final short TA_KEY_TEXT_SHADOW_COLOR = 19; + public static final short TA_KEY_IS_HIGHLIGHTED = 20; + public static final short TA_KEY_LAYOUT_DIRECTION = 21; + public static final short TA_KEY_ACCESSIBILITY_ROLE = 22; + public static final int UNSET = -1; private static final String PROP_SHADOW_OFFSET = "textShadowOffset"; @@ -61,7 +90,7 @@ public class TextAttributeProps { // `UNSET` is -1 and is the same as `LayoutDirection.UNDEFINED` but the symbol isn't available. protected int mLayoutDirection = UNSET; - protected TextTransform mTextTransform = TextTransform.UNSET; + protected TextTransform mTextTransform = TextTransform.NONE; protected float mTextShadowOffsetDx = 0; protected float mTextShadowOffsetDy = 0; @@ -111,36 +140,123 @@ public class TextAttributeProps { protected boolean mContainsImages = false; protected float mHeightOfTallestInlineImage = Float.NaN; - private final ReactStylesDiffMap mProps; - - public TextAttributeProps(ReactStylesDiffMap props) { - mProps = props; - setNumberOfLines(getIntProp(ViewProps.NUMBER_OF_LINES, UNSET)); - setLineHeight(getFloatProp(ViewProps.LINE_HEIGHT, UNSET)); - setLetterSpacing(getFloatProp(ViewProps.LETTER_SPACING, Float.NaN)); - setAllowFontScaling(getBooleanProp(ViewProps.ALLOW_FONT_SCALING, true)); - setFontSize(getFloatProp(ViewProps.FONT_SIZE, UNSET)); - setColor(props.hasKey(ViewProps.COLOR) ? props.getInt(ViewProps.COLOR, 0) : null); - setColor( + private TextAttributeProps() {} + + /** + * Build a TextAttributeProps using data from the {@link ReadableMapBuffer} received as a + * parameter. + */ + public static TextAttributeProps fromReadableMapBuffer(ReadableMapBuffer props) { + TextAttributeProps result = new TextAttributeProps(); + + // TODO T83483191: Review constants that are not being set! + Iterator iterator = props.iterator(); + while (iterator.hasNext()) { + ReadableMapBuffer.MapBufferEntry entry = iterator.next(); + switch (entry.getKey()) { + case TA_KEY_FOREGROUND_COLOR: + result.setColor(entry.getInt(0)); + break; + case TA_KEY_BACKGROUND_COLOR: + result.setBackgroundColor(entry.getInt(0)); + break; + case TA_KEY_OPACITY: + break; + case TA_KEY_FONT_FAMILY: + result.setFontFamily(entry.getString()); + break; + case TA_KEY_FONT_SIZE: + result.setFontSize((float) entry.getDouble(UNSET)); + break; + case TA_KEY_FONT_SIZE_MULTIPLIER: + break; + case TA_KEY_FONT_WEIGHT: + result.setFontWeight(entry.getString()); + break; + case TA_KEY_FONT_STYLE: + result.setFontStyle(entry.getString()); + break; + case TA_KEY_FONT_VARIANT: + result.setFontVariant(entry.getReadableMapBuffer()); + break; + case TA_KEY_ALLOW_FONT_SCALING: + result.setAllowFontScaling(entry.getBoolean(true)); + break; + case TA_KEY_LETTER_SPACING: + result.setLetterSpacing((float) entry.getDouble(Float.NaN)); + break; + case TA_KEY_LINE_HEIGHT: + result.setLineHeight((float) entry.getDouble(UNSET)); + break; + case TA_KEY_ALIGNMENT: + break; + case TA_KEY_BEST_WRITING_DIRECTION: + break; + case TA_KEY_TEXT_DECORATION_COLOR: + break; + case TA_KEY_TEXT_DECORATION_LINE: + result.setTextDecorationLine(entry.getString()); + break; + case TA_KEY_TEXT_DECORATION_LINE_STYLE: + break; + case TA_KEY_TEXT_DECORATION_LINE_PATTERN: + break; + case TA_KEY_TEXT_SHADOW_RAIDUS: + result.setTextShadowRadius(entry.getInt(1)); + break; + case TA_KEY_TEXT_SHADOW_COLOR: + result.setTextShadowColor(entry.getInt(DEFAULT_TEXT_SHADOW_COLOR)); + break; + case TA_KEY_IS_HIGHLIGHTED: + break; + case TA_KEY_LAYOUT_DIRECTION: + result.setLayoutDirection(entry.getString()); + break; + case TA_KEY_ACCESSIBILITY_ROLE: + result.setAccessibilityRole(entry.getString()); + break; + } + } + + // TODO T83483191: Review why the following props are not serialized: + // setNumberOfLines + // setColor + // setIncludeFontPadding + // setTextShadowOffset + // setTextTransform + return result; + } + + public static TextAttributeProps fromReadableMap(ReactStylesDiffMap props) { + TextAttributeProps result = new TextAttributeProps(); + result.setNumberOfLines(getIntProp(props, ViewProps.NUMBER_OF_LINES, UNSET)); + result.setLineHeight(getFloatProp(props, ViewProps.LINE_HEIGHT, UNSET)); + result.setLetterSpacing(getFloatProp(props, ViewProps.LETTER_SPACING, Float.NaN)); + result.setAllowFontScaling(getBooleanProp(props, ViewProps.ALLOW_FONT_SCALING, true)); + result.setFontSize(getFloatProp(props, ViewProps.FONT_SIZE, UNSET)); + result.setColor(props.hasKey(ViewProps.COLOR) ? props.getInt(ViewProps.COLOR, 0) : null); + result.setColor( props.hasKey(ViewProps.FOREGROUND_COLOR) ? props.getInt(ViewProps.FOREGROUND_COLOR, 0) : null); - setBackgroundColor( + result.setBackgroundColor( props.hasKey(ViewProps.BACKGROUND_COLOR) ? props.getInt(ViewProps.BACKGROUND_COLOR, 0) : null); - setFontFamily(getStringProp(ViewProps.FONT_FAMILY)); - setFontWeight(getStringProp(ViewProps.FONT_WEIGHT)); - setFontStyle(getStringProp(ViewProps.FONT_STYLE)); - setFontVariant(getArrayProp(ViewProps.FONT_VARIANT)); - setIncludeFontPadding(getBooleanProp(ViewProps.INCLUDE_FONT_PADDING, true)); - setTextDecorationLine(getStringProp(ViewProps.TEXT_DECORATION_LINE)); - setTextShadowOffset(props.hasKey(PROP_SHADOW_OFFSET) ? props.getMap(PROP_SHADOW_OFFSET) : null); - setTextShadowRadius(getIntProp(PROP_SHADOW_RADIUS, 1)); - setTextShadowColor(getIntProp(PROP_SHADOW_COLOR, DEFAULT_TEXT_SHADOW_COLOR)); - setTextTransform(getStringProp(PROP_TEXT_TRANSFORM)); - setLayoutDirection(getStringProp(ViewProps.LAYOUT_DIRECTION)); - setAccessibilityRole(getStringProp(ViewProps.ACCESSIBILITY_ROLE)); + result.setFontFamily(getStringProp(props, ViewProps.FONT_FAMILY)); + result.setFontWeight(getStringProp(props, ViewProps.FONT_WEIGHT)); + result.setFontStyle(getStringProp(props, ViewProps.FONT_STYLE)); + result.setFontVariant(getArrayProp(props, ViewProps.FONT_VARIANT)); + result.setIncludeFontPadding(getBooleanProp(props, ViewProps.INCLUDE_FONT_PADDING, true)); + result.setTextDecorationLine(getStringProp(props, ViewProps.TEXT_DECORATION_LINE)); + result.setTextShadowOffset( + props.hasKey(PROP_SHADOW_OFFSET) ? props.getMap(PROP_SHADOW_OFFSET) : null); + result.setTextShadowRadius(getIntProp(props, PROP_SHADOW_RADIUS, 1)); + result.setTextShadowColor(getIntProp(props, PROP_SHADOW_COLOR, DEFAULT_TEXT_SHADOW_COLOR)); + result.setTextTransform(getStringProp(props, PROP_TEXT_TRANSFORM)); + result.setLayoutDirection(getStringProp(props, ViewProps.LAYOUT_DIRECTION)); + result.setAccessibilityRole(getStringProp(props, ViewProps.ACCESSIBILITY_ROLE)); + return result; } public static int getTextAlignment(ReactStylesDiffMap props, boolean isRTL) { @@ -178,7 +294,8 @@ public static int getJustificationMode(ReactStylesDiffMap props) { return DEFAULT_JUSTIFICATION_MODE; } - private boolean getBooleanProp(String name, boolean defaultValue) { + private static boolean getBooleanProp( + ReactStylesDiffMap mProps, String name, boolean defaultValue) { if (mProps.hasKey(name)) { return mProps.getBoolean(name, defaultValue); } else { @@ -186,7 +303,7 @@ private boolean getBooleanProp(String name, boolean defaultValue) { } } - private String getStringProp(String name) { + private static String getStringProp(ReactStylesDiffMap mProps, String name) { if (mProps.hasKey(name)) { return mProps.getString(name); } else { @@ -194,7 +311,7 @@ private String getStringProp(String name) { } } - private int getIntProp(String name, int defaultvalue) { + private static int getIntProp(ReactStylesDiffMap mProps, String name, int defaultvalue) { if (mProps.hasKey(name)) { return mProps.getInt(name, defaultvalue); } else { @@ -202,7 +319,7 @@ private int getIntProp(String name, int defaultvalue) { } } - private float getFloatProp(String name, float defaultvalue) { + private static float getFloatProp(ReactStylesDiffMap mProps, String name, float defaultvalue) { if (mProps.hasKey(name)) { return mProps.getFloat(name, defaultvalue); } else { @@ -210,7 +327,7 @@ private float getFloatProp(String name, float defaultvalue) { } } - private @Nullable ReadableArray getArrayProp(String name) { + private static @Nullable ReadableArray getArrayProp(ReactStylesDiffMap mProps, String name) { if (mProps.hasKey(name)) { return mProps.getArray(name); } else { @@ -309,6 +426,40 @@ private void setFontVariant(@Nullable ReadableArray fontVariant) { mFontFeatureSettings = ReactTypefaceUtils.parseFontVariant(fontVariant); } + private void setFontVariant(@Nullable ReadableMapBuffer fontVariant) { + if (fontVariant == null || fontVariant.getCount() == 0) { + mFontFeatureSettings = null; + return; + } + + List features = new ArrayList<>(); + Iterator iterator = fontVariant.iterator(); + while (iterator.hasNext()) { + ReadableMapBuffer.MapBufferEntry entry = iterator.next(); + String value = entry.getString(); + if (value != null) { + switch (value) { + case "small-caps": + features.add("'smcp'"); + break; + case "oldstyle-nums": + features.add("'onum'"); + break; + case "lining-nums": + features.add("'lnum'"); + break; + case "tabular-nums": + features.add("'tnum'"); + break; + case "proportional-nums": + features.add("'pnum'"); + break; + } + } + } + mFontFeatureSettings = TextUtils.join(", ", features); + } + /** * /* This code is duplicated in ReactTextInputManager /* TODO: Factor into a common place they * can both use @@ -380,17 +531,23 @@ private void setTextShadowOffset(ReadableMap offsetMap) { } } - private void setLayoutDirection(@Nullable String layoutDirection) { + public static int getLayoutDirection(@Nullable String layoutDirection) { + int androidLayoutDirection; if (layoutDirection == null || "undefined".equals(layoutDirection)) { - mLayoutDirection = UNSET; + androidLayoutDirection = UNSET; } else if ("rtl".equals(layoutDirection)) { - mLayoutDirection = LayoutDirection.RTL; + androidLayoutDirection = LayoutDirection.RTL; } else if ("ltr".equals(layoutDirection)) { - mLayoutDirection = LayoutDirection.LTR; + androidLayoutDirection = LayoutDirection.LTR; } else { throw new JSApplicationIllegalArgumentException( "Invalid layoutDirection: " + layoutDirection); } + return androidLayoutDirection; + } + + private void setLayoutDirection(@Nullable String layoutDirection) { + mLayoutDirection = getLayoutDirection(layoutDirection); } private void setTextShadowRadius(float textShadowRadius) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java index c2f428075f451d..0b88a8a28d9fbb 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManager.java @@ -70,11 +70,11 @@ public class TextLayoutManager { public static boolean isRTL(ReadableMap attributedString) { ReadableArray fragments = attributedString.getArray("fragments"); - for (int i = 0, length = fragments.size(); i < length; i++) { + for (int i = 0; i < fragments.size(); i++) { ReadableMap fragment = fragments.getMap(i); - ReactStylesDiffMap map = new ReactStylesDiffMap(fragment.getMap("textAttributes")); - TextAttributeProps textAttributes = new TextAttributeProps(map); - return textAttributes.mLayoutDirection == LayoutDirection.RTL; + ReadableMap map = fragment.getMap("textAttributes"); + return TextAttributeProps.getLayoutDirection(map.getString(ViewProps.LAYOUT_DIRECTION)) + == LayoutDirection.RTL; } return false; } @@ -105,7 +105,8 @@ private static void buildSpannableFromFragment( // ReactRawText TextAttributeProps textAttributes = - new TextAttributeProps(new ReactStylesDiffMap(fragment.getMap("textAttributes"))); + TextAttributeProps.fromReadableMap( + new ReactStylesDiffMap(fragment.getMap("textAttributes"))); sb.append(TextTransform.apply(fragment.getString("string"), textAttributes.mTextTransform)); diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java new file mode 100644 index 00000000000000..7af9a540f8644f --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/views/text/TextLayoutManagerMapBuffer.java @@ -0,0 +1,588 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +package com.facebook.react.views.text; + +import static com.facebook.react.views.text.TextAttributeProps.UNSET; + +import android.content.Context; +import android.os.Build; +import android.text.BoringLayout; +import android.text.Layout; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.StaticLayout; +import android.text.TextPaint; +import android.util.LayoutDirection; +import android.util.LruCache; +import android.view.View; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.WritableArray; +import com.facebook.react.common.build.ReactBuildConfig; +import com.facebook.react.common.mapbuffer.ReadableMapBuffer; +import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ReactAccessibilityDelegate; +import com.facebook.yoga.YogaConstants; +import com.facebook.yoga.YogaMeasureMode; +import com.facebook.yoga.YogaMeasureOutput; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; + +/** Class responsible of creating {@link Spanned} object for the JS representation of Text */ +public class TextLayoutManagerMapBuffer { + + // constants for AttributedString serialization + public static final short AS_KEY_HASH = 0; + public static final short AS_KEY_STRING = 1; + public static final short AS_KEY_FRAGMENTS = 2; + public static final short AS_KEY_CACHE_ID = 3; + + // constants for Fragment serialization + public static final short FR_KEY_STRING = 0; + public static final short FR_KEY_REACT_TAG = 1; + public static final short FR_KEY_IS_ATTACHMENT = 2; + public static final short FR_KEY_WIDTH = 3; + public static final short FR_KEY_HEIGHT = 4; + public static final short FR_KEY_TEXT_ATTRIBUTES = 5; + + // constants for ParagraphAttributes serialization + public static final short PA_KEY_MAX_NUMBER_OF_LINES = 0; + public static final short PA_KEY_ELLIPSIZE_MODE = 1; + public static final short PA_KEY_TEXT_BREAK_STRATEGY = 2; + public static final short PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3; + public static final short PA_KEY_INCLUDE_FONT_PADDING = 4; + + private static final boolean ENABLE_MEASURE_LOGGING = ReactBuildConfig.DEBUG && false; + + private static final String TAG = TextLayoutManagerMapBuffer.class.getSimpleName(); + + // It's important to pass the ANTI_ALIAS_FLAG flag to the constructor rather than setting it + // later by calling setFlags. This is because the latter approach triggers a bug on Android 4.4.2. + // The bug is that unicode emoticons aren't measured properly which causes text to be clipped. + private static final TextPaint sTextPaintInstance = new TextPaint(TextPaint.ANTI_ALIAS_FLAG); + + // Specifies the amount of spannable that are stored into the {@link sSpannableCache}. + private static final short spannableCacheSize = 100; + + private static final String INLINE_VIEW_PLACEHOLDER = "0"; + + private static final Object sSpannableCacheLock = new Object(); + private static final boolean DEFAULT_INCLUDE_FONT_PADDING = true; + private static final LruCache sSpannableCache = + new LruCache<>(spannableCacheSize); + private static final ConcurrentHashMap sTagToSpannableCache = + new ConcurrentHashMap<>(); + + public static void setCachedSpannabledForTag(int reactTag, @NonNull Spannable sp) { + if (ENABLE_MEASURE_LOGGING) { + FLog.e(TAG, "Set cached spannable for tag[" + reactTag + "]: " + sp.toString()); + } + sTagToSpannableCache.put(reactTag, sp); + } + + public static void deleteCachedSpannableForTag(int reactTag) { + if (ENABLE_MEASURE_LOGGING) { + FLog.e(TAG, "Delete cached spannable for tag[" + reactTag + "]"); + } + sTagToSpannableCache.remove(reactTag); + } + + public static boolean isRTL(ReadableMapBuffer attributedString) { + ReadableMapBuffer fragments = attributedString.getMapBuffer(AS_KEY_FRAGMENTS); + if (fragments.getCount() == 0) { + return false; + } + + ReadableMapBuffer fragment = fragments.getMapBuffer((short) 0); + ReadableMapBuffer textAttributes = fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES); + return TextAttributeProps.getLayoutDirection( + textAttributes.getString(TextAttributeProps.TA_KEY_LAYOUT_DIRECTION)) + == LayoutDirection.RTL; + } + + private static void buildSpannableFromFragment( + Context context, + ReadableMapBuffer fragments, + SpannableStringBuilder sb, + List ops) { + + for (short i = 0, length = fragments.getCount(); i < length; i++) { + ReadableMapBuffer fragment = fragments.getMapBuffer(i); + int start = sb.length(); + + TextAttributeProps textAttributes = + TextAttributeProps.fromReadableMapBuffer(fragment.getMapBuffer(FR_KEY_TEXT_ATTRIBUTES)); + + sb.append( + TextTransform.apply(fragment.getString(FR_KEY_STRING), textAttributes.mTextTransform)); + + int end = sb.length(); + int reactTag = + fragment.hasKey(FR_KEY_REACT_TAG) ? fragment.getInt(FR_KEY_REACT_TAG) : View.NO_ID; + if (fragment.hasKey(FR_KEY_IS_ATTACHMENT) && fragment.getBoolean(FR_KEY_IS_ATTACHMENT)) { + float width = PixelUtil.toPixelFromSP(fragment.getDouble(FR_KEY_WIDTH)); + float height = PixelUtil.toPixelFromSP(fragment.getDouble(FR_KEY_HEIGHT)); + ops.add( + new SetSpanOperation( + sb.length() - INLINE_VIEW_PLACEHOLDER.length(), + sb.length(), + new TextInlineViewPlaceholderSpan(reactTag, (int) width, (int) height))); + } else if (end >= start) { + if (ReactAccessibilityDelegate.AccessibilityRole.LINK.equals( + textAttributes.mAccessibilityRole)) { + ops.add( + new SetSpanOperation( + start, end, new ReactClickableSpan(reactTag, textAttributes.mColor))); + } else if (textAttributes.mIsColorSet) { + ops.add( + new SetSpanOperation( + start, end, new ReactForegroundColorSpan(textAttributes.mColor))); + } + if (textAttributes.mIsBackgroundColorSet) { + ops.add( + new SetSpanOperation( + start, end, new ReactBackgroundColorSpan(textAttributes.mBackgroundColor))); + } + if (!Float.isNaN(textAttributes.getLetterSpacing())) { + ops.add( + new SetSpanOperation( + start, end, new CustomLetterSpacingSpan(textAttributes.getLetterSpacing()))); + } + ops.add( + new SetSpanOperation(start, end, new ReactAbsoluteSizeSpan(textAttributes.mFontSize))); + if (textAttributes.mFontStyle != UNSET + || textAttributes.mFontWeight != UNSET + || textAttributes.mFontFamily != null) { + ops.add( + new SetSpanOperation( + start, + end, + new CustomStyleSpan( + textAttributes.mFontStyle, + textAttributes.mFontWeight, + textAttributes.mFontFeatureSettings, + textAttributes.mFontFamily, + context.getAssets()))); + } + if (textAttributes.mIsUnderlineTextDecorationSet) { + ops.add(new SetSpanOperation(start, end, new ReactUnderlineSpan())); + } + if (textAttributes.mIsLineThroughTextDecorationSet) { + ops.add(new SetSpanOperation(start, end, new ReactStrikethroughSpan())); + } + if (textAttributes.mTextShadowOffsetDx != 0 || textAttributes.mTextShadowOffsetDy != 0) { + ops.add( + new SetSpanOperation( + start, + end, + new ShadowStyleSpan( + textAttributes.mTextShadowOffsetDx, + textAttributes.mTextShadowOffsetDy, + textAttributes.mTextShadowRadius, + textAttributes.mTextShadowColor))); + } + if (!Float.isNaN(textAttributes.getEffectiveLineHeight())) { + ops.add( + new SetSpanOperation( + start, end, new CustomLineHeightSpan(textAttributes.getEffectiveLineHeight()))); + } + + ops.add(new SetSpanOperation(start, end, new ReactTagSpan(reactTag))); + } + } + } + + // public because both ReactTextViewManager and ReactTextInputManager need to use this + public static Spannable getOrCreateSpannableForText( + Context context, + ReadableMapBuffer attributedString, + @Nullable ReactTextViewManagerCallback reactTextViewManagerCallback) { + + Spannable preparedSpannableText; + + synchronized (sSpannableCacheLock) { + preparedSpannableText = sSpannableCache.get(attributedString); + if (preparedSpannableText != null) { + return preparedSpannableText; + } + } + + preparedSpannableText = + createSpannableFromAttributedString( + context, attributedString, reactTextViewManagerCallback); + + synchronized (sSpannableCacheLock) { + sSpannableCache.put(attributedString, preparedSpannableText); + } + + return preparedSpannableText; + } + + private static Spannable createSpannableFromAttributedString( + Context context, + ReadableMapBuffer attributedString, + @Nullable ReactTextViewManagerCallback reactTextViewManagerCallback) { + + SpannableStringBuilder sb = new SpannableStringBuilder(); + + // The {@link SpannableStringBuilder} implementation require setSpan operation to be called + // up-to-bottom, otherwise all the spannables that are within the region for which one may set + // a new spannable will be wiped out + List ops = new ArrayList<>(); + + buildSpannableFromFragment(context, attributedString.getMapBuffer(AS_KEY_FRAGMENTS), sb, ops); + + // TODO T31905686: add support for inline Images + // While setting the Spans on the final text, we also check whether any of them are images. + int priority = 0; + for (SetSpanOperation op : ops) { + // Actual order of calling {@code execute} does NOT matter, + // but the {@code priority} DOES matter. + op.execute(sb, priority); + priority++; + } + + if (reactTextViewManagerCallback != null) { + reactTextViewManagerCallback.onPostProcessSpannable(sb); + } + return sb; + } + + private static Layout createLayout( + Spannable text, + BoringLayout.Metrics boring, + float width, + YogaMeasureMode widthYogaMeasureMode, + boolean includeFontPadding, + int textBreakStrategy) { + Layout layout; + int spanLength = text.length(); + boolean unconstrainedWidth = widthYogaMeasureMode == YogaMeasureMode.UNDEFINED || width < 0; + TextPaint textPaint = sTextPaintInstance; + float desiredWidth = boring == null ? Layout.getDesiredWidth(text, textPaint) : Float.NaN; + + if (boring == null + && (unconstrainedWidth + || (!YogaConstants.isUndefined(desiredWidth) && desiredWidth <= width))) { + // Is used when the width is not known and the text is not boring, ie. if it contains + // unicode characters. + + int hintWidth = (int) Math.ceil(desiredWidth); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + layout = + new StaticLayout( + text, + textPaint, + hintWidth, + Layout.Alignment.ALIGN_NORMAL, + 1.f, + 0.f, + includeFontPadding); + } else { + layout = + StaticLayout.Builder.obtain(text, 0, spanLength, textPaint, hintWidth) + .setAlignment(Layout.Alignment.ALIGN_NORMAL) + .setLineSpacing(0.f, 1.f) + .setIncludePad(includeFontPadding) + .setBreakStrategy(textBreakStrategy) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL) + .build(); + } + + } else if (boring != null && (unconstrainedWidth || boring.width <= width)) { + // Is used for single-line, boring text when the width is either unknown or bigger + // than the width of the text. + layout = + BoringLayout.make( + text, + textPaint, + boring.width, + Layout.Alignment.ALIGN_NORMAL, + 1.f, + 0.f, + boring, + includeFontPadding); + } else { + // Is used for multiline, boring text and the width is known. + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { + layout = + new StaticLayout( + text, + textPaint, + (int) width, + Layout.Alignment.ALIGN_NORMAL, + 1.f, + 0.f, + includeFontPadding); + } else { + StaticLayout.Builder builder = + StaticLayout.Builder.obtain(text, 0, spanLength, textPaint, (int) width) + .setAlignment(Layout.Alignment.ALIGN_NORMAL) + .setLineSpacing(0.f, 1.f) + .setIncludePad(includeFontPadding) + .setBreakStrategy(textBreakStrategy) + .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + builder.setUseLineSpacingFromFallbacks(true); + } + + layout = builder.build(); + } + } + return layout; + } + + public static long measureText( + Context context, + ReadableMapBuffer attributedString, + ReadableMapBuffer paragraphAttributes, + float width, + YogaMeasureMode widthYogaMeasureMode, + float height, + YogaMeasureMode heightYogaMeasureMode, + ReactTextViewManagerCallback reactTextViewManagerCallback, + @Nullable float[] attachmentsPositions) { + + // TODO(5578671): Handle text direction (see View#getTextDirectionHeuristic) + TextPaint textPaint = sTextPaintInstance; + Spannable text; + if (attributedString.hasKey(AS_KEY_CACHE_ID)) { + int cacheId = attributedString.getInt(AS_KEY_CACHE_ID); + if (ENABLE_MEASURE_LOGGING) { + FLog.e(TAG, "Get cached spannable for cacheId[" + cacheId + "]"); + } + if (sTagToSpannableCache.containsKey(cacheId)) { + text = sTagToSpannableCache.get(cacheId); + if (ENABLE_MEASURE_LOGGING) { + FLog.e(TAG, "Text for spannable found for cacheId[" + cacheId + "]: " + text.toString()); + } + } else { + if (ENABLE_MEASURE_LOGGING) { + FLog.e(TAG, "No cached spannable found for cacheId[" + cacheId + "]"); + } + return 0; + } + } else { + text = getOrCreateSpannableForText(context, attributedString, reactTextViewManagerCallback); + } + + int textBreakStrategy = + TextAttributeProps.getTextBreakStrategy( + paragraphAttributes.getString(PA_KEY_TEXT_BREAK_STRATEGY)); + boolean includeFontPadding = + paragraphAttributes.hasKey(PA_KEY_INCLUDE_FONT_PADDING) + ? paragraphAttributes.getBoolean(PA_KEY_INCLUDE_FONT_PADDING) + : DEFAULT_INCLUDE_FONT_PADDING; + + if (text == null) { + throw new IllegalStateException("Spannable element has not been prepared in onBeforeLayout"); + } + + BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint); + float desiredWidth = boring == null ? Layout.getDesiredWidth(text, textPaint) : Float.NaN; + + // technically, width should never be negative, but there is currently a bug in + boolean unconstrainedWidth = widthYogaMeasureMode == YogaMeasureMode.UNDEFINED || width < 0; + + Layout layout = + createLayout( + text, boring, width, widthYogaMeasureMode, includeFontPadding, textBreakStrategy); + + int maximumNumberOfLines = + paragraphAttributes.hasKey(PA_KEY_MAX_NUMBER_OF_LINES) + ? paragraphAttributes.getInt(PA_KEY_MAX_NUMBER_OF_LINES) + : UNSET; + + int calculatedLineCount = + maximumNumberOfLines == UNSET || maximumNumberOfLines == 0 + ? layout.getLineCount() + : Math.min(maximumNumberOfLines, layout.getLineCount()); + + // Instead of using `layout.getWidth()` (which may yield a significantly larger width for + // text that is wrapping), compute width using the longest line. + float calculatedWidth = 0; + if (widthYogaMeasureMode == YogaMeasureMode.EXACTLY) { + calculatedWidth = width; + } else { + for (int lineIndex = 0; lineIndex < calculatedLineCount; lineIndex++) { + float lineWidth = layout.getLineWidth(lineIndex); + if (lineWidth > calculatedWidth) { + calculatedWidth = lineWidth; + } + } + if (widthYogaMeasureMode == YogaMeasureMode.AT_MOST && calculatedWidth > width) { + calculatedWidth = width; + } + } + + float calculatedHeight = height; + if (heightYogaMeasureMode != YogaMeasureMode.EXACTLY) { + calculatedHeight = layout.getLineBottom(calculatedLineCount - 1); + if (heightYogaMeasureMode == YogaMeasureMode.AT_MOST && calculatedHeight > height) { + calculatedHeight = height; + } + } + + // Calculate the positions of the attachments (views) that will be rendered inside the + // Spanned Text. The following logic is only executed when a text contains views inside. + // This follows a similar logic than used in pre-fabric (see ReactTextView.onLayout method). + int attachmentIndex = 0; + int lastAttachmentFoundInSpan; + for (int i = 0; i < text.length(); i = lastAttachmentFoundInSpan) { + lastAttachmentFoundInSpan = + text.nextSpanTransition(i, text.length(), TextInlineViewPlaceholderSpan.class); + TextInlineViewPlaceholderSpan[] placeholders = + text.getSpans(i, lastAttachmentFoundInSpan, TextInlineViewPlaceholderSpan.class); + for (TextInlineViewPlaceholderSpan placeholder : placeholders) { + int start = text.getSpanStart(placeholder); + int line = layout.getLineForOffset(start); + boolean isLineTruncated = layout.getEllipsisCount(line) > 0; + // This truncation check works well on recent versions of Android (tested on 5.1.1 and + // 6.0.1) but not on Android 4.4.4. The reason is that getEllipsisCount is buggy on + // Android 4.4.4. Specifically, it incorrectly returns 0 if an inline view is the + // first thing to be truncated. + if (!(isLineTruncated && start >= layout.getLineStart(line) + layout.getEllipsisStart(line)) + || start >= layout.getLineEnd(line)) { + float placeholderWidth = placeholder.getWidth(); + float placeholderHeight = placeholder.getHeight(); + // Calculate if the direction of the placeholder character is Right-To-Left. + boolean isRtlChar = layout.isRtlCharAt(start); + boolean isRtlParagraph = layout.getParagraphDirection(line) == Layout.DIR_RIGHT_TO_LEFT; + float placeholderLeftPosition; + // There's a bug on Samsung devices where calling getPrimaryHorizontal on + // the last offset in the layout will result in an endless loop. Work around + // this bug by avoiding getPrimaryHorizontal in that case. + if (start == text.length() - 1) { + placeholderLeftPosition = + isRtlParagraph + // Equivalent to `layout.getLineLeft(line)` but `getLineLeft` returns + // incorrect + // values when the paragraph is RTL and `setSingleLine(true)`. + ? calculatedWidth - layout.getLineWidth(line) + : layout.getLineRight(line) - placeholderWidth; + } else { + // The direction of the paragraph may not be exactly the direction the string is + // heading + // in at the + // position of the placeholder. So, if the direction of the character is the same + // as the + // paragraph + // use primary, secondary otherwise. + boolean characterAndParagraphDirectionMatch = isRtlParagraph == isRtlChar; + placeholderLeftPosition = + characterAndParagraphDirectionMatch + ? layout.getPrimaryHorizontal(start) + : layout.getSecondaryHorizontal(start); + if (isRtlParagraph) { + // Adjust `placeholderLeftPosition` to work around an Android bug. + // The bug is when the paragraph is RTL and `setSingleLine(true)`, some layout + // methods such as `getPrimaryHorizontal`, `getSecondaryHorizontal`, and + // `getLineRight` return incorrect values. Their return values seem to be off + // by the same number of pixels so subtracting these values cancels out the + // error. + // + // The result is equivalent to bugless versions of + // `getPrimaryHorizontal`/`getSecondaryHorizontal`. + placeholderLeftPosition = + calculatedWidth - (layout.getLineRight(line) - placeholderLeftPosition); + } + if (isRtlChar) { + placeholderLeftPosition -= placeholderWidth; + } + } + // Vertically align the inline view to the baseline of the line of text. + float placeholderTopPosition = layout.getLineBaseline(line) - placeholderHeight; + int attachmentPosition = attachmentIndex * 2; + + // The attachment array returns the positions of each of the attachments as + attachmentsPositions[attachmentPosition] = + PixelUtil.toSPFromPixel(placeholderTopPosition); + attachmentsPositions[attachmentPosition + 1] = + PixelUtil.toSPFromPixel(placeholderLeftPosition); + attachmentIndex++; + } + } + } + + float widthInSP = PixelUtil.toSPFromPixel(calculatedWidth); + float heightInSP = PixelUtil.toSPFromPixel(calculatedHeight); + + if (ENABLE_MEASURE_LOGGING) { + FLog.e( + TAG, + "TextMeasure call ('" + + text + + "'): w: " + + calculatedWidth + + " px - h: " + + calculatedHeight + + " px - w : " + + widthInSP + + " sp - h: " + + heightInSP + + " sp"); + } + + return YogaMeasureOutput.make(widthInSP, heightInSP); + } + + public static WritableArray measureLines( + @NonNull Context context, + ReadableMapBuffer attributedString, + ReadableMapBuffer paragraphAttributes, + float width) { + + TextPaint textPaint = sTextPaintInstance; + Spannable text = getOrCreateSpannableForText(context, attributedString, null); + BoringLayout.Metrics boring = BoringLayout.isBoring(text, textPaint); + + int textBreakStrategy = + TextAttributeProps.getTextBreakStrategy( + paragraphAttributes.getString(PA_KEY_TEXT_BREAK_STRATEGY)); + boolean includeFontPadding = + paragraphAttributes.hasKey(PA_KEY_INCLUDE_FONT_PADDING) + ? paragraphAttributes.getBoolean(PA_KEY_INCLUDE_FONT_PADDING) + : DEFAULT_INCLUDE_FONT_PADDING; + + Layout layout = + createLayout( + text, boring, width, YogaMeasureMode.EXACTLY, includeFontPadding, textBreakStrategy); + return FontMetricsUtil.getFontMetrics(text, layout, sTextPaintInstance, context); + } + + // TODO T31905686: This class should be private + public static class SetSpanOperation { + protected int start, end; + protected ReactSpan what; + + public SetSpanOperation(int start, int end, ReactSpan what) { + this.start = start; + this.end = end; + this.what = what; + } + + public void execute(Spannable sb, int priority) { + // All spans will automatically extend to the right of the text, but not the left - except + // for spans that start at the beginning of the text. + int spanFlags = Spannable.SPAN_EXCLUSIVE_INCLUSIVE; + if (start == 0) { + spanFlags = Spannable.SPAN_INCLUSIVE_INCLUSIVE; + } + + spanFlags &= ~Spannable.SPAN_PRIORITY; + spanFlags |= (priority << Spannable.SPAN_PRIORITY_SHIFT) & Spannable.SPAN_PRIORITY; + + sb.setSpan(what, start, end, spanFlags); + } + } +} diff --git a/ReactAndroid/src/main/jni/react/jni/Android.mk b/ReactAndroid/src/main/jni/react/jni/Android.mk index 9831c1aeef2c0e..8b24a2fdae571d 100644 --- a/ReactAndroid/src/main/jni/react/jni/Android.mk +++ b/ReactAndroid/src/main/jni/react/jni/Android.mk @@ -135,6 +135,7 @@ include $(REACT_SRC_DIR)/reactperflogger/jni/Android.mk # Note: Update this only when ready to minimize breaking changes. include $(REACT_SRC_DIR)/turbomodule/core/jni/Android.mk include $(REACT_SRC_DIR)/fabric/jni/Android.mk +include $(REACT_SRC_DIR)/common/mapbuffer/jni/Android.mk # TODO(ramanpreet): # Why doesn't this import-module call generate a jscexecutor.so file? diff --git a/ReactCommon/react/renderer/attributedstring/Android.mk b/ReactCommon/react/renderer/attributedstring/Android.mk index 2b90b0378380cb..1eee7957d277b5 100644 --- a/ReactCommon/react/renderer/attributedstring/Android.mk +++ b/ReactCommon/react/renderer/attributedstring/Android.mk @@ -21,7 +21,7 @@ LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall LOCAL_STATIC_LIBRARIES := -LOCAL_SHARED_LIBRARIES := libbetter libreact_render_graphics libyoga libfolly_futures glog libfolly_json libglog_init libreact_render_core libreact_render_debug librrc_view libreact_utils libreact_debug +LOCAL_SHARED_LIBRARIES := libbetter libreact_render_graphics libyoga libfolly_futures glog libfolly_json libglog_init libreact_render_core libreact_render_debug librrc_view libreact_utils libreact_debug libreact_render_mapbuffer include $(BUILD_SHARED_LIBRARY) @@ -37,3 +37,4 @@ $(call import-module,react/renderer/graphics) $(call import-module,react/utils) $(call import-module,react/debug) $(call import-module,yogajni) +$(call import-module,react/renderer/mapbuffer) diff --git a/ReactCommon/react/renderer/attributedstring/BUCK b/ReactCommon/react/renderer/attributedstring/BUCK index a793db14709e85..9010d94c8834c0 100644 --- a/ReactCommon/react/renderer/attributedstring/BUCK +++ b/ReactCommon/react/renderer/attributedstring/BUCK @@ -37,6 +37,9 @@ rn_xplat_cxx_library( "-std=c++14", "-Wall", ], + fbandroid_deps = [ + react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"), + ], fbobjc_compiler_flags = APPLE_COMPILER_FLAGS, fbobjc_preprocessor_flags = get_preprocessor_flags_for_build_mode() + get_apple_inspector_flags(), force_static = True, diff --git a/ReactCommon/react/renderer/attributedstring/conversions.h b/ReactCommon/react/renderer/attributedstring/conversions.h index 203cc367ec8015..f1bce4debe1844 100644 --- a/ReactCommon/react/renderer/attributedstring/conversions.h +++ b/ReactCommon/react/renderer/attributedstring/conversions.h @@ -23,6 +23,11 @@ #include #include +#ifdef ANDROID +#include +#include +#endif + #include namespace facebook { @@ -39,6 +44,7 @@ inline std::string toString(const EllipsizeMode &ellipsisMode) { case EllipsizeMode::Middle: return "middle"; } + abort(); } inline void fromRawValue(const RawValue &value, EllipsizeMode &result) { @@ -71,6 +77,7 @@ inline std::string toString(const TextBreakStrategy &textBreakStrategy) { case TextBreakStrategy::Balanced: return "balanced"; } + abort(); } inline void fromRawValue(const RawValue &value, TextBreakStrategy &result) { @@ -173,6 +180,7 @@ inline std::string toString(const FontStyle &fontStyle) { case FontStyle::Oblique: return "oblique"; } + abort(); } inline void fromRawValue(const RawValue &value, FontVariant &result) { @@ -267,6 +275,7 @@ inline std::string toString(const TextAlignment &textAlignment) { case TextAlignment::Justified: return "justified"; } + abort(); } inline void fromRawValue(const RawValue &value, WritingDirection &result) { @@ -295,6 +304,7 @@ inline std::string toString(const WritingDirection &writingDirection) { case WritingDirection::RightToLeft: return "rtl"; } + abort(); } inline void fromRawValue( @@ -337,6 +347,7 @@ inline std::string toString( case TextDecorationLineType::UnderlineStrikethrough: return "underline-strikethrough"; } + abort(); } inline void fromRawValue( @@ -368,6 +379,7 @@ inline std::string toString( case TextDecorationLineStyle::Double: return "double"; } + abort(); } inline void fromRawValue( @@ -411,6 +423,7 @@ inline std::string toString( case TextDecorationLinePattern::DashDotDot: return "dash-dot-dot"; } + abort(); } inline std::string toString(const AccessibilityRole &accessibilityRole) { @@ -470,6 +483,7 @@ inline std::string toString(const AccessibilityRole &accessibilityRole) { case AccessibilityRole::Toolbar: return "toolbar"; } + abort(); } inline void fromRawValue(const RawValue &value, AccessibilityRole &result) { @@ -809,6 +823,224 @@ inline folly::dynamic toDynamic(AttributedString::Range const &range) { return dynamicValue; } +// constants for AttributedString serialization +constexpr static Key AS_KEY_HASH = 0; +constexpr static Key AS_KEY_STRING = 1; +constexpr static Key AS_KEY_FRAGMENTS = 2; +constexpr static Key AS_KEY_CACHE_ID = 3; + +// constants for Fragment serialization +constexpr static Key FR_KEY_STRING = 0; +constexpr static Key FR_KEY_REACT_TAG = 1; +constexpr static Key FR_KEY_IS_ATTACHMENT = 2; +constexpr static Key FR_KEY_WIDTH = 3; +constexpr static Key FR_KEY_HEIGHT = 4; +constexpr static Key FR_KEY_TEXT_ATTRIBUTES = 5; + +// constants for Text Attributes serialization +constexpr static Key TA_KEY_FOREGROUND_COLOR = 0; +constexpr static Key TA_KEY_BACKGROUND_COLOR = 1; +constexpr static Key TA_KEY_OPACITY = 2; +constexpr static Key TA_KEY_FONT_FAMILY = 3; +constexpr static Key TA_KEY_FONT_SIZE = 4; +constexpr static Key TA_KEY_FONT_SIZE_MULTIPLIER = 5; +constexpr static Key TA_KEY_FONT_WEIGHT = 6; +constexpr static Key TA_KEY_FONT_STYLE = 7; +constexpr static Key TA_KEY_FONT_VARIANT = 8; +constexpr static Key TA_KEY_ALLOW_FONT_SCALING = 9; +constexpr static Key TA_KEY_LETTER_SPACING = 10; +constexpr static Key TA_KEY_LINE_HEIGHT = 11; +constexpr static Key TA_KEY_ALIGNMENT = 12; +constexpr static Key TA_KEY_BEST_WRITING_DIRECTION = 13; +constexpr static Key TA_KEY_TEXT_DECORATION_COLOR = 14; +constexpr static Key TA_KEY_TEXT_DECORATION_LINE = 15; +constexpr static Key TA_KEY_TEXT_DECORATION_LINE_STYLE = 16; +constexpr static Key TA_KEY_TEXT_DECORATION_LINE_PATTERN = 17; +constexpr static Key TA_KEY_TEXT_SHADOW_RAIDUS = 18; +constexpr static Key TA_KEY_TEXT_SHADOW_COLOR = 19; +constexpr static Key TA_KEY_IS_HIGHLIGHTED = 20; +constexpr static Key TA_KEY_LAYOUT_DIRECTION = 21; +constexpr static Key TA_KEY_ACCESSIBILITY_ROLE = 22; + +// constants for ParagraphAttributes serialization +constexpr static Key PA_KEY_MAX_NUMBER_OF_LINES = 0; +constexpr static Key PA_KEY_ELLIPSIZE_MODE = 1; +constexpr static Key PA_KEY_TEXT_BREAK_STRATEGY = 2; +constexpr static Key PA_KEY_ADJUST_FONT_SIZE_TO_FIT = 3; +constexpr static Key PA_KEY_INCLUDE_FONT_PADDING = 4; + +inline MapBuffer toMapBuffer(const ParagraphAttributes ¶graphAttributes) { + auto builder = MapBufferBuilder(); + builder.putInt( + PA_KEY_MAX_NUMBER_OF_LINES, paragraphAttributes.maximumNumberOfLines); + builder.putString( + PA_KEY_ELLIPSIZE_MODE, toString(paragraphAttributes.ellipsizeMode)); + builder.putString( + PA_KEY_TEXT_BREAK_STRATEGY, + toString(paragraphAttributes.textBreakStrategy)); + builder.putBool( + PA_KEY_ADJUST_FONT_SIZE_TO_FIT, paragraphAttributes.adjustsFontSizeToFit); + builder.putBool( + PA_KEY_INCLUDE_FONT_PADDING, paragraphAttributes.includeFontPadding); + + return builder.build(); +} + +inline MapBuffer toMapBuffer(const FontVariant &fontVariant) { + auto builder = MapBufferBuilder(); + int index = 0; + if ((int)fontVariant & (int)FontVariant::SmallCaps) { + builder.putString(index++, "small-caps"); + } + if ((int)fontVariant & (int)FontVariant::OldstyleNums) { + builder.putString(index++, "oldstyle-nums"); + } + if ((int)fontVariant & (int)FontVariant::LiningNums) { + builder.putString(index++, "lining-nums"); + } + if ((int)fontVariant & (int)FontVariant::TabularNums) { + builder.putString(index++, "tabular-nums"); + } + if ((int)fontVariant & (int)FontVariant::ProportionalNums) { + builder.putString(index++, "proportional-nums"); + } + + return builder.build(); +} + +inline MapBuffer toMapBuffer(const TextAttributes &textAttributes) { + auto builder = MapBufferBuilder(); + if (textAttributes.foregroundColor) { + builder.putInt( + TA_KEY_FOREGROUND_COLOR, toMapBuffer(textAttributes.foregroundColor)); + } + if (textAttributes.backgroundColor) { + builder.putInt( + TA_KEY_BACKGROUND_COLOR, toMapBuffer(textAttributes.backgroundColor)); + } + if (!std::isnan(textAttributes.opacity)) { + builder.putDouble(TA_KEY_OPACITY, textAttributes.opacity); + } + if (!textAttributes.fontFamily.empty()) { + builder.putString(TA_KEY_FONT_FAMILY, textAttributes.fontFamily); + } + if (!std::isnan(textAttributes.fontSize)) { + builder.putDouble(TA_KEY_FONT_SIZE, textAttributes.fontSize); + } + if (!std::isnan(textAttributes.fontSizeMultiplier)) { + builder.putDouble( + TA_KEY_FONT_SIZE_MULTIPLIER, textAttributes.fontSizeMultiplier); + } + if (textAttributes.fontWeight.has_value()) { + builder.putString(TA_KEY_FONT_WEIGHT, toString(*textAttributes.fontWeight)); + } + if (textAttributes.fontStyle.has_value()) { + builder.putString(TA_KEY_FONT_STYLE, toString(*textAttributes.fontStyle)); + } + if (textAttributes.fontVariant.has_value()) { + auto fontVariantMap = toMapBuffer(*textAttributes.fontVariant); + builder.putMapBuffer(TA_KEY_FONT_VARIANT, fontVariantMap); + } + if (textAttributes.allowFontScaling.has_value()) { + builder.putBool( + TA_KEY_ALLOW_FONT_SCALING, *textAttributes.allowFontScaling); + } + if (!std::isnan(textAttributes.letterSpacing)) { + builder.putDouble(TA_KEY_LETTER_SPACING, textAttributes.letterSpacing); + } + if (!std::isnan(textAttributes.lineHeight)) { + builder.putDouble(TA_KEY_LINE_HEIGHT, textAttributes.lineHeight); + } + if (textAttributes.alignment.has_value()) { + builder.putString(TA_KEY_ALIGNMENT, toString(*textAttributes.alignment)); + } + if (textAttributes.baseWritingDirection.has_value()) { + builder.putString( + TA_KEY_BEST_WRITING_DIRECTION, + toString(*textAttributes.baseWritingDirection)); + } + // Decoration + if (textAttributes.textDecorationColor) { + builder.putInt( + TA_KEY_TEXT_DECORATION_COLOR, + toMapBuffer(textAttributes.textDecorationColor)); + } + if (textAttributes.textDecorationLineType.has_value()) { + builder.putString( + TA_KEY_TEXT_DECORATION_LINE, + toString(*textAttributes.textDecorationLineType)); + } + if (textAttributes.textDecorationLineStyle.has_value()) { + builder.putString( + TA_KEY_TEXT_DECORATION_LINE_STYLE, + toString(*textAttributes.textDecorationLineStyle)); + } + if (textAttributes.textDecorationLinePattern.has_value()) { + builder.putString( + TA_KEY_TEXT_DECORATION_LINE_PATTERN, + toString(*textAttributes.textDecorationLinePattern)); + } + // Shadow + if (!std::isnan(textAttributes.textShadowRadius)) { + builder.putDouble( + TA_KEY_TEXT_SHADOW_RAIDUS, textAttributes.textShadowRadius); + } + if (textAttributes.textShadowColor) { + builder.putInt( + TA_KEY_TEXT_SHADOW_COLOR, toMapBuffer(textAttributes.textShadowColor)); + } + // Special + if (textAttributes.isHighlighted.has_value()) { + builder.putBool(TA_KEY_IS_HIGHLIGHTED, *textAttributes.isHighlighted); + } + if (textAttributes.layoutDirection.has_value()) { + builder.putString( + TA_KEY_LAYOUT_DIRECTION, toString(*textAttributes.layoutDirection)); + } + if (textAttributes.accessibilityRole.has_value()) { + builder.putString( + TA_KEY_ACCESSIBILITY_ROLE, toString(*textAttributes.accessibilityRole)); + } + return builder.build(); +} + +inline MapBuffer toMapBuffer(const AttributedString &attributedString) { + auto fragmentsBuilder = MapBufferBuilder(); + + int index = 0; + for (auto fragment : attributedString.getFragments()) { + auto dynamicFragmentBuilder = MapBufferBuilder(); + dynamicFragmentBuilder.putString(FR_KEY_STRING, fragment.string); + if (fragment.parentShadowView.componentHandle) { + dynamicFragmentBuilder.putInt( + FR_KEY_REACT_TAG, fragment.parentShadowView.tag); + } + if (fragment.isAttachment()) { + dynamicFragmentBuilder.putBool(FR_KEY_IS_ATTACHMENT, true); + dynamicFragmentBuilder.putDouble( + FR_KEY_WIDTH, + fragment.parentShadowView.layoutMetrics.frame.size.width); + dynamicFragmentBuilder.putDouble( + FR_KEY_HEIGHT, + fragment.parentShadowView.layoutMetrics.frame.size.height); + } + auto textAttributesMap = toMapBuffer(fragment.textAttributes); + dynamicFragmentBuilder.putMapBuffer( + FR_KEY_TEXT_ATTRIBUTES, textAttributesMap); + auto dynamicFragmentMap = dynamicFragmentBuilder.build(); + fragmentsBuilder.putMapBuffer(index++, dynamicFragmentMap); + } + + auto builder = MapBufferBuilder(); + builder.putInt( + AS_KEY_HASH, + std::hash{}(attributedString)); + builder.putString(AS_KEY_STRING, attributedString.getString()); + auto fragmentsMap = fragmentsBuilder.build(); + builder.putMapBuffer(AS_KEY_FRAGMENTS, fragmentsMap); + return builder.build(); +} + #endif } // namespace react diff --git a/ReactCommon/react/renderer/components/image/Android.mk b/ReactCommon/react/renderer/components/image/Android.mk index eeaa35d4e74ae6..7fa492f3438b8d 100644 --- a/ReactCommon/react/renderer/components/image/Android.mk +++ b/ReactCommon/react/renderer/components/image/Android.mk @@ -21,7 +21,7 @@ LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall LOCAL_STATIC_LIBRARIES := -LOCAL_SHARED_LIBRARIES := libyoga glog libfolly_json libglog_init libreact_render_core libreact_render_debug libreact_render_graphics librrc_view libreact_render_imagemanager libreact_debug +LOCAL_SHARED_LIBRARIES := libyoga glog libfolly_json libglog_init libreact_render_core libreact_render_debug libreact_render_graphics librrc_view libreact_render_imagemanager libreact_debug libreact_render_mapbuffer include $(BUILD_SHARED_LIBRARY) @@ -35,3 +35,4 @@ $(call import-module,react/renderer/imagemanager) $(call import-module,react/renderer/components/view) $(call import-module,yogajni) $(call import-module,react/debug) +$(call import-module,react/renderer/mapbuffer) diff --git a/ReactCommon/react/renderer/components/image/BUCK b/ReactCommon/react/renderer/components/image/BUCK index 512859a6e39aaa..00017fddf53a86 100644 --- a/ReactCommon/react/renderer/components/image/BUCK +++ b/ReactCommon/react/renderer/components/image/BUCK @@ -35,6 +35,9 @@ rn_xplat_cxx_library( "-std=c++14", "-Wall", ], + fbandroid_deps = [ + react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"), + ], fbobjc_compiler_flags = APPLE_COMPILER_FLAGS, fbobjc_preprocessor_flags = get_preprocessor_flags_for_build_mode() + get_apple_inspector_flags(), labels = ["supermodule:xplat/default/public.react_native.infra"], diff --git a/ReactCommon/react/renderer/components/image/ImageState.h b/ReactCommon/react/renderer/components/image/ImageState.h index 8dfa5d6f902173..68cdcb681f4fd8 100644 --- a/ReactCommon/react/renderer/components/image/ImageState.h +++ b/ReactCommon/react/renderer/components/image/ImageState.h @@ -7,10 +7,14 @@ #pragma once -#include #include #include +#ifdef ANDROID +#include +#include +#endif + namespace facebook { namespace react { @@ -50,6 +54,10 @@ class ImageState final { folly::dynamic getDynamic() const { return {}; }; + + MapBuffer getMapBuffer() const { + return MapBufferBuilder::EMPTY(); + }; #endif private: diff --git a/ReactCommon/react/renderer/components/modal/Android.mk b/ReactCommon/react/renderer/components/modal/Android.mk index 8594c9d8208ca0..ea2dddf664a6cc 100644 --- a/ReactCommon/react/renderer/components/modal/Android.mk +++ b/ReactCommon/react/renderer/components/modal/Android.mk @@ -21,7 +21,7 @@ LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall LOCAL_STATIC_LIBRARIES := -LOCAL_SHARED_LIBRARIES := libyoga glog libfolly_json libglog_init libreact_render_core libreact_render_debug libreact_render_graphics librrc_image libreact_render_uimanager libreact_render_imagemanager librrc_view libreact_render_componentregistry libreact_codegen_rncore +LOCAL_SHARED_LIBRARIES := libyoga glog libfolly_json libglog_init libreact_render_core libreact_render_debug libreact_render_graphics librrc_image libreact_render_uimanager libreact_render_imagemanager librrc_view libreact_render_componentregistry libreact_codegen_rncore libreact_render_mapbuffer include $(BUILD_SHARED_LIBRARY) @@ -37,3 +37,4 @@ $(call import-module,react/renderer/uimanager) $(call import-module,react/renderer/components/image) $(call import-module,react/renderer/components/view) $(call import-module,yogajni) +$(call import-module,react/renderer/mapbuffer) diff --git a/ReactCommon/react/renderer/components/modal/BUCK b/ReactCommon/react/renderer/components/modal/BUCK index 3eb48ca4cb6cdd..8a1dbba208353d 100644 --- a/ReactCommon/react/renderer/components/modal/BUCK +++ b/ReactCommon/react/renderer/components/modal/BUCK @@ -37,6 +37,9 @@ rn_xplat_cxx_library( "-std=c++14", "-Wall", ], + fbandroid_deps = [ + react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"), + ], fbandroid_exported_headers = subdir_glob( [ ("", "*.h"), diff --git a/ReactCommon/react/renderer/components/modal/ModalHostViewState.h b/ReactCommon/react/renderer/components/modal/ModalHostViewState.h index 92ab4c5afb7e40..b0921864ba7791 100644 --- a/ReactCommon/react/renderer/components/modal/ModalHostViewState.h +++ b/ReactCommon/react/renderer/components/modal/ModalHostViewState.h @@ -13,6 +13,8 @@ #ifdef ANDROID #include +#include +#include #endif namespace facebook { @@ -41,6 +43,10 @@ class ModalHostViewState final { #ifdef ANDROID folly::dynamic getDynamic() const; + MapBuffer getMapBuffer() const { + return MapBufferBuilder::EMPTY(); + }; + #endif #pragma mark - Getters diff --git a/ReactCommon/react/renderer/components/scrollview/Android.mk b/ReactCommon/react/renderer/components/scrollview/Android.mk index 8726fbea871b8c..5e0748c0e4917f 100644 --- a/ReactCommon/react/renderer/components/scrollview/Android.mk +++ b/ReactCommon/react/renderer/components/scrollview/Android.mk @@ -21,7 +21,7 @@ LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall LOCAL_STATIC_LIBRARIES := -LOCAL_SHARED_LIBRARIES := libyoga libfolly_futures glog libfolly_json libglog_init libreact_render_core libreact_render_debug libreact_render_graphics librrc_view libreact_debug +LOCAL_SHARED_LIBRARIES := libyoga libfolly_futures glog libfolly_json libglog_init libreact_render_core libreact_render_debug libreact_render_graphics librrc_view libreact_debug libreact_render_mapbuffer include $(BUILD_SHARED_LIBRARY) @@ -34,3 +34,4 @@ $(call import-module,react/renderer/debug) $(call import-module,react/renderer/graphics) $(call import-module,react/debug) $(call import-module,yogajni) +$(call import-module,react/renderer/mapbuffer) diff --git a/ReactCommon/react/renderer/components/scrollview/BUCK b/ReactCommon/react/renderer/components/scrollview/BUCK index ab356b049788cc..f9fb72f3594146 100644 --- a/ReactCommon/react/renderer/components/scrollview/BUCK +++ b/ReactCommon/react/renderer/components/scrollview/BUCK @@ -38,6 +38,9 @@ rn_xplat_cxx_library( "-std=c++14", "-Wall", ], + fbandroid_deps = [ + react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"), + ], fbobjc_compiler_flags = APPLE_COMPILER_FLAGS, fbobjc_preprocessor_flags = get_preprocessor_flags_for_build_mode() + get_apple_inspector_flags(), force_static = True, diff --git a/ReactCommon/react/renderer/components/scrollview/ScrollViewState.h b/ReactCommon/react/renderer/components/scrollview/ScrollViewState.h index a2ea7b945f81c0..3f979ab3f597b9 100644 --- a/ReactCommon/react/renderer/components/scrollview/ScrollViewState.h +++ b/ReactCommon/react/renderer/components/scrollview/ScrollViewState.h @@ -9,7 +9,11 @@ #include +#ifdef ANDROID #include +#include +#include +#endif namespace facebook { namespace react { @@ -39,6 +43,9 @@ class ScrollViewState final { return folly::dynamic::object("contentOffsetLeft", contentOffset.x)( "contentOffsetTop", contentOffset.y); }; + MapBuffer getMapBuffer() const { + return MapBufferBuilder::EMPTY(); + }; #endif }; diff --git a/ReactCommon/react/renderer/components/slider/Android.mk b/ReactCommon/react/renderer/components/slider/Android.mk index d64d8a9cb6dab4..34c4368b56f27e 100644 --- a/ReactCommon/react/renderer/components/slider/Android.mk +++ b/ReactCommon/react/renderer/components/slider/Android.mk @@ -21,7 +21,7 @@ LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall LOCAL_STATIC_LIBRARIES := -LOCAL_SHARED_LIBRARIES := libfbjni libreact_codegen_rncore libreact_render_imagemanager libreactnativeutilsjni libreact_render_componentregistry libreact_render_uimanager librrc_image libyoga libfolly_futures glog libfolly_json libglog_init libreact_render_core libreact_render_debug libreact_render_graphics librrc_view libreact_debug +LOCAL_SHARED_LIBRARIES := libfbjni libreact_codegen_rncore libreact_render_imagemanager libreactnativeutilsjni libreact_render_componentregistry libreact_render_uimanager librrc_image libyoga libfolly_futures glog libfolly_json libglog_init libreact_render_core libreact_render_debug libreact_render_graphics librrc_view libreact_debug libreact_render_mapbuffer include $(BUILD_SHARED_LIBRARY) @@ -38,3 +38,4 @@ $(call import-module,react/renderer/components/image) $(call import-module,react/renderer/components/view) $(call import-module,react/renderer/uimanager) $(call import-module,yogajni) +$(call import-module,react/renderer/mapbuffer) diff --git a/ReactCommon/react/renderer/components/slider/BUCK b/ReactCommon/react/renderer/components/slider/BUCK index 412f0443b3e7bc..59f5a5cc27d695 100644 --- a/ReactCommon/react/renderer/components/slider/BUCK +++ b/ReactCommon/react/renderer/components/slider/BUCK @@ -42,6 +42,7 @@ rn_xplat_cxx_library( cxx_tests = [":tests"], fbandroid_deps = [ react_native_target("jni/react/jni:jni"), + react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"), ], fbandroid_exported_headers = subdir_glob( [ diff --git a/ReactCommon/react/renderer/components/slider/SliderState.h b/ReactCommon/react/renderer/components/slider/SliderState.h index 4aa4927faf64c1..82a338c08f24ee 100644 --- a/ReactCommon/react/renderer/components/slider/SliderState.h +++ b/ReactCommon/react/renderer/components/slider/SliderState.h @@ -7,7 +7,12 @@ #pragma once +#ifdef ANDROID #include +#include +#include +#endif + #include #include @@ -64,6 +69,9 @@ class SliderState final { folly::dynamic getDynamic() const { return {}; }; + MapBuffer getMapBuffer() const { + return MapBufferBuilder::EMPTY(); + }; #endif private: diff --git a/ReactCommon/react/renderer/components/text/Android.mk b/ReactCommon/react/renderer/components/text/Android.mk index 160867b6ac9067..7acbcced7ae74b 100644 --- a/ReactCommon/react/renderer/components/text/Android.mk +++ b/ReactCommon/react/renderer/components/text/Android.mk @@ -21,7 +21,7 @@ LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall LOCAL_STATIC_LIBRARIES := -LOCAL_SHARED_LIBRARIES := libyoga glog libfolly_json libglog_init libreact_render_core libreact_render_debug libreact_render_graphics libreact_render_uimanager libreact_render_textlayoutmanager libreact_render_attributedstring libreact_render_mounting librrc_view libreact_utils libreact_debug +LOCAL_SHARED_LIBRARIES := libyoga glog libfolly_json libglog_init libreact_render_core libreact_render_debug libreact_render_graphics libreact_render_uimanager libreact_render_textlayoutmanager libreact_render_attributedstring libreact_render_mounting librrc_view libreact_utils libreact_debug libreact_render_mapbuffer include $(BUILD_SHARED_LIBRARY) @@ -38,4 +38,5 @@ $(call import-module,react/renderer/uimanager) $(call import-module,react/renderer/components/view) $(call import-module,react/utils) $(call import-module,react/debug) +$(call import-module,react/renderer/mapbuffer) $(call import-module,yogajni) diff --git a/ReactCommon/react/renderer/components/text/BUCK b/ReactCommon/react/renderer/components/text/BUCK index 1a38a17d874945..9b467be70585be 100644 --- a/ReactCommon/react/renderer/components/text/BUCK +++ b/ReactCommon/react/renderer/components/text/BUCK @@ -43,6 +43,9 @@ rn_xplat_cxx_library( "-Wall", ], cxx_tests = [":tests"], + fbandroid_deps = [ + react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"), + ], fbobjc_compiler_flags = APPLE_COMPILER_FLAGS, fbobjc_preprocessor_flags = get_preprocessor_flags_for_build_mode() + get_apple_inspector_flags(), force_static = True, diff --git a/ReactCommon/react/renderer/components/text/ParagraphState.cpp b/ReactCommon/react/renderer/components/text/ParagraphState.cpp index c91bb6e480f41e..5c15d2a31e6492 100644 --- a/ReactCommon/react/renderer/components/text/ParagraphState.cpp +++ b/ReactCommon/react/renderer/components/text/ParagraphState.cpp @@ -17,6 +17,10 @@ namespace react { folly::dynamic ParagraphState::getDynamic() const { return toDynamic(*this); } + +MapBuffer ParagraphState::getMapBuffer() const { + return toMapBuffer(*this); +} #endif } // namespace react diff --git a/ReactCommon/react/renderer/components/text/ParagraphState.h b/ReactCommon/react/renderer/components/text/ParagraphState.h index b31b8798d847ed..d4e80fda9cd7c7 100644 --- a/ReactCommon/react/renderer/components/text/ParagraphState.h +++ b/ReactCommon/react/renderer/components/text/ParagraphState.h @@ -14,6 +14,7 @@ #ifdef ANDROID #include +#include #endif namespace facebook { @@ -60,6 +61,7 @@ class ParagraphState final { react_native_assert(false && "Not supported"); }; folly::dynamic getDynamic() const; + MapBuffer getMapBuffer() const; #endif }; diff --git a/ReactCommon/react/renderer/components/text/conversions.h b/ReactCommon/react/renderer/components/text/conversions.h index 0d44cd762143c6..8ea5baeddb7978 100644 --- a/ReactCommon/react/renderer/components/text/conversions.h +++ b/ReactCommon/react/renderer/components/text/conversions.h @@ -8,6 +8,10 @@ #include #include #include +#ifdef ANDROID +#include +#include +#endif namespace facebook { namespace react { @@ -21,6 +25,24 @@ inline folly::dynamic toDynamic(ParagraphState const ¶graphState) { newState["hash"] = newState["attributedString"]["hash"]; return newState; } + +// constants for Text State serialization +constexpr static Key TX_STATE_KEY_ATTRIBUTED_STRING = 0; +constexpr static Key TX_STATE_KEY_PARAGRAPH_ATTRIBUTES = 1; +// Used for TextInput +constexpr static Key TX_STATE_KEY_HASH = 2; +constexpr static Key TX_STATE_KEY_MOST_RECENT_EVENT_COUNT = 3; + +inline MapBuffer toMapBuffer(ParagraphState const ¶graphState) { + auto builder = MapBufferBuilder(); + auto attStringMapBuffer = toMapBuffer(paragraphState.attributedString); + builder.putMapBuffer(TX_STATE_KEY_ATTRIBUTED_STRING, attStringMapBuffer); + auto paMapBuffer = toMapBuffer(paragraphState.paragraphAttributes); + builder.putMapBuffer(TX_STATE_KEY_PARAGRAPH_ATTRIBUTES, paMapBuffer); + // TODO: Used for TextInput + builder.putInt(TX_STATE_KEY_HASH, 1234); + return builder.build(); +} #endif } // namespace react diff --git a/ReactCommon/react/renderer/components/textinput/Android.mk b/ReactCommon/react/renderer/components/textinput/Android.mk index 729aa7de908756..694e9042ea372b 100644 --- a/ReactCommon/react/renderer/components/textinput/Android.mk +++ b/ReactCommon/react/renderer/components/textinput/Android.mk @@ -21,7 +21,7 @@ LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall LOCAL_STATIC_LIBRARIES := -LOCAL_SHARED_LIBRARIES := libyoga glog libfolly_json libglog_init libreact_render_core libreact_render_mounting libreact_render_componentregistry libreact_render_debug libreact_render_graphics libreact_render_uimanager libreact_render_imagemanager libreact_render_textlayoutmanager libreact_render_attributedstring librrc_text librrc_image librrc_view libreact_utils libreact_debug +LOCAL_SHARED_LIBRARIES := libyoga glog libfolly_json libglog_init libreact_render_core libreact_render_mounting libreact_render_componentregistry libreact_render_debug libreact_render_graphics libreact_render_uimanager libreact_render_imagemanager libreact_render_textlayoutmanager libreact_render_attributedstring librrc_text librrc_image librrc_view libreact_utils libreact_debug libreact_render_mapbuffer include $(BUILD_SHARED_LIBRARY) @@ -43,3 +43,4 @@ $(call import-module,react/renderer/components/text) $(call import-module,react/utils) $(call import-module,react/debug) $(call import-module,yogajni) +$(call import-module,react/renderer/mapbuffer) diff --git a/ReactCommon/react/renderer/components/textinput/BUCK b/ReactCommon/react/renderer/components/textinput/BUCK index 76373d0d8e0c4d..ed0615e0bda0b9 100644 --- a/ReactCommon/react/renderer/components/textinput/BUCK +++ b/ReactCommon/react/renderer/components/textinput/BUCK @@ -40,6 +40,9 @@ rn_xplat_cxx_library( "-Wall", ], cxx_tests = [":tests"], + fbandroid_deps = [ + react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"), + ], fbobjc_compiler_flags = APPLE_COMPILER_FLAGS, fbobjc_preprocessor_flags = get_preprocessor_flags_for_build_mode() + get_apple_inspector_flags(), force_static = True, diff --git a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.h b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.h index a52188f9ead565..4d3d41ac7a87c6 100644 --- a/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.h +++ b/ReactCommon/react/renderer/components/textinput/androidtextinput/react/renderer/components/androidtextinput/AndroidTextInputState.h @@ -13,6 +13,8 @@ #ifdef ANDROID #include +#include +#include #endif namespace facebook { @@ -92,6 +94,9 @@ class AndroidTextInputState final { AndroidTextInputState const &previousState, folly::dynamic const &data); folly::dynamic getDynamic() const; + MapBuffer getMapBuffer() const { + return MapBufferBuilder::EMPTY(); + }; }; } // namespace react diff --git a/ReactCommon/react/renderer/components/textinput/iostextinput/TextInputState.h b/ReactCommon/react/renderer/components/textinput/iostextinput/TextInputState.h index 31ea9c77a2aa03..fb249f4a840ab3 100644 --- a/ReactCommon/react/renderer/components/textinput/iostextinput/TextInputState.h +++ b/ReactCommon/react/renderer/components/textinput/iostextinput/TextInputState.h @@ -13,6 +13,8 @@ #ifdef ANDROID #include +#include +#include #endif namespace facebook { diff --git a/ReactCommon/react/renderer/core/Android.mk b/ReactCommon/react/renderer/core/Android.mk index b6787cd9cd8d10..2ffdde0f44d50f 100644 --- a/ReactCommon/react/renderer/core/Android.mk +++ b/ReactCommon/react/renderer/core/Android.mk @@ -30,3 +30,4 @@ $(call import-module,react/utils) $(call import-module,react/debug) $(call import-module,react/renderer/debug) $(call import-module,react/renderer/graphics) +$(call import-module,react/renderer/mapbuffer) diff --git a/ReactCommon/react/renderer/core/ConcreteState.h b/ReactCommon/react/renderer/core/ConcreteState.h index f4ebd03b35ee17..c4f8bef37e1d2b 100644 --- a/ReactCommon/react/renderer/core/ConcreteState.h +++ b/ReactCommon/react/renderer/core/ConcreteState.h @@ -105,6 +105,9 @@ class ConcreteState : public State { void updateState(folly::dynamic data) const override { updateState(std::move(Data(getData(), data))); } + MapBuffer getMapBuffer() const override { + return getData().getMapBuffer(); + } #endif }; diff --git a/ReactCommon/react/renderer/core/State.h b/ReactCommon/react/renderer/core/State.h index c0472fec057bf5..ce5b51fe065e03 100644 --- a/ReactCommon/react/renderer/core/State.h +++ b/ReactCommon/react/renderer/core/State.h @@ -9,6 +9,8 @@ #ifdef ANDROID #include +#include +#include #endif #include @@ -65,6 +67,7 @@ class State { #ifdef ANDROID virtual folly::dynamic getDynamic() const = 0; + virtual MapBuffer getMapBuffer() const = 0; virtual void updateState(folly::dynamic data) const = 0; #endif diff --git a/ReactCommon/react/renderer/core/StateData.h b/ReactCommon/react/renderer/core/StateData.h index b13b5950490b87..36e098d1cd647d 100644 --- a/ReactCommon/react/renderer/core/StateData.h +++ b/ReactCommon/react/renderer/core/StateData.h @@ -11,6 +11,8 @@ #ifdef ANDROID #include +#include +#include #endif namespace facebook { @@ -27,6 +29,7 @@ struct StateData final { StateData() = default; StateData(StateData const &previousState, folly::dynamic data){}; folly::dynamic getDynamic() const; + MapBuffer getMapBuffer() const; #endif }; diff --git a/ReactCommon/react/renderer/graphics/conversions.h b/ReactCommon/react/renderer/graphics/conversions.h index 1f910dd07096ca..f3420295b2ed57 100644 --- a/ReactCommon/react/renderer/graphics/conversions.h +++ b/ReactCommon/react/renderer/graphics/conversions.h @@ -58,6 +58,16 @@ inline folly::dynamic toDynamic(const SharedColor &color) { ((int)round(components.blue * ratio) & 0xff)); } +inline int toMapBuffer(const SharedColor &color) { + ColorComponents components = colorComponentsFromColor(color); + auto ratio = 255.f; + return ( + ((int)round(components.alpha * ratio) & 0xff) << 24 | + ((int)round(components.red * ratio) & 0xff) << 16 | + ((int)round(components.green * ratio) & 0xff) << 8 | + ((int)round(components.blue * ratio) & 0xff)); +} + #endif inline std::string toString(const SharedColor &value) { diff --git a/ReactCommon/react/renderer/mapbuffer/Android.mk b/ReactCommon/react/renderer/mapbuffer/Android.mk index f6c9584ba33510..65bf3e9e2cf08a 100644 --- a/ReactCommon/react/renderer/mapbuffer/Android.mk +++ b/ReactCommon/react/renderer/mapbuffer/Android.mk @@ -9,21 +9,21 @@ include $(CLEAR_VARS) LOCAL_MODULE := react_render_mapbuffer -LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../../ - LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp) +LOCAL_C_INCLUDES := $(LOCAL_PATH)/../../../ LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/../../../ -LOCAL_SHARED_LIBRARIES := libreact_utils glog libglog_init - LOCAL_CFLAGS := \ -DLOG_TAG=\"Fabric\" LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall +LOCAL_STATIC_LIBRARIES := + +LOCAL_SHARED_LIBRARIES := glog libglog_init + include $(BUILD_SHARED_LIBRARY) -$(call import-module,react/utils) $(call import-module,glog) $(call import-module,fbgloginit) diff --git a/ReactCommon/react/renderer/mapbuffer/BUCK b/ReactCommon/react/renderer/mapbuffer/BUCK index dc246215845732..39bdca768f8a7f 100644 --- a/ReactCommon/react/renderer/mapbuffer/BUCK +++ b/ReactCommon/react/renderer/mapbuffer/BUCK @@ -44,7 +44,6 @@ rn_xplat_cxx_library( "//xplat/fbsystrace:fbsystrace", "//xplat/folly:headers_only", "//xplat/folly:memory", - react_native_xplat_target("react/utils:utils"), ], ) diff --git a/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp b/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp index 1de973515531f3..dfed3a200a60af 100644 --- a/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp +++ b/ReactCommon/react/renderer/mapbuffer/MapBuffer.cpp @@ -12,78 +12,31 @@ using namespace facebook::react; namespace facebook { namespace react { -MapBuffer::MapBuffer(uint16_t initialSize) { - _dataSize = initialSize; - _data = new Byte[_dataSize]; - // TODO: Should we clean up memory here? -} +MapBuffer::MapBuffer(uint8_t *const data, uint16_t dataSize) { + // Should we move the memory here or document it? + _data = data; -void MapBuffer::makeSpace() { - int oldDataSize = _dataSize; - if (_dataSize >= std::numeric_limits::max() / 2) { - LOG(ERROR) - << "Error: trying to assign a value beyond the capacity of uint16_t" - << static_cast(_dataSize) * 2; - throw "Error: trying to assign a value beyond the capacity of uint16_t" + - std::to_string(static_cast(_dataSize) * 2); - } - _dataSize *= 2; - uint8_t *_newdata = new Byte[_dataSize]; - uint8_t *_oldData = _data; - memcpy(_newdata, _data, oldDataSize); - _data = _newdata; - delete[] _oldData; -} + _count = 0; + memcpy( + reinterpret_cast(&_count), + reinterpret_cast(_data + HEADER_COUNT_OFFSET), + UINT16_SIZE); -void MapBuffer::putBytes(Key key, uint8_t *value, int valueSize) { - if (key != _header.count) { - LOG(ERROR) - << "Error: key out of order (for now keys should we stored contiguous) " - << key; - throw "Error: key out of order (for now keys should we stored contiguous) - key: " + - std::to_string(key); - } + // TODO: extract memcpy calls into an inline function to simplify the code + _dataSize = 0; + memcpy( + reinterpret_cast(&_dataSize), + reinterpret_cast(_data + HEADER_BUFFER_SIZE_OFFSET), + UINT16_SIZE); - int valueOffset = getValueOffset(key); - if (valueOffset + valueSize > _dataSize) { - makeSpace(); + if (dataSize != _dataSize) { + LOG(ERROR) << "Error: Data size does not match, expected " << dataSize + << " found: " << _dataSize; + throw "Error: Data size does not match"; } - - memcpy(_data + getKeyOffset(key), &key, KEY_SIZE); - memcpy(_data + valueOffset, value, valueSize); - _header.count++; } -void MapBuffer::putBool(Key key, bool value) { - putInt(key, (int)value); -} - -void MapBuffer::putDouble(Key key, double value) { - uint8_t *bytePointer = reinterpret_cast(&value); - putBytes(key, bytePointer, DOUBLE_SIZE); -} - -void MapBuffer::putNull(Key key) { - putInt(key, NULL_VALUE); -} - -void MapBuffer::putInt(Key key, int value) { - uint8_t *bytePointer = reinterpret_cast(&(value)); - putBytes(key, bytePointer, INT_SIZE); -} - -void MapBuffer::finish() { - // Copy header at the beginning of "_data" - memcpy(_data, &_header, HEADER_SIZE); - // TODO: create a MapBufferBuilder instead of calling the finish method. -} - -// TODO: All the "getXXX" methods are currently operating on a "finished" map. -// Next step: create a MapBufferBuilder, move "putXXX" methods into the -// MapBufferBuilder, make MapBuffer class immutable. -int MapBuffer::getInt(Key key) { - checkKeyConsistency(_header, _data, key); - +int MapBuffer::getInt(Key key) const { int value = 0; memcpy( reinterpret_cast(&value), @@ -92,13 +45,11 @@ int MapBuffer::getInt(Key key) { return value; } -bool MapBuffer::getBool(Key key) { +bool MapBuffer::getBool(Key key) const { return getInt(key) != 0; } -double MapBuffer::getDouble(Key key) { - checkKeyConsistency(_header, _data, key); - +double MapBuffer::getDouble(Key key) const { // TODO: extract this code into a "template method" and reuse it for other // types double value = 0; @@ -109,26 +60,81 @@ double MapBuffer::getDouble(Key key) { return value; } -bool MapBuffer::isNull(Key key) { +int MapBuffer::getDynamicDataOffset() const { + // The begininig of dynamic data can be calculated as the offset of the next + // key in the map + return getKeyOffset(_count); +} + +std::string MapBuffer::getString(Key key) const { + // TODO Add checks to verify that offsets are under the boundaries of the map + // buffer + int dynamicDataOffset = getDynamicDataOffset(); + int stringLength = 0; + memcpy( + reinterpret_cast(&stringLength), + reinterpret_cast(_data + dynamicDataOffset), + INT_SIZE); + + int valueOffset = getInt(key) + sizeof(stringLength); + + char *value = new char[stringLength]; + + memcpy( + reinterpret_cast(value), + reinterpret_cast(_data + dynamicDataOffset + valueOffset), + stringLength); + + return std::string(value); +} + +MapBuffer MapBuffer::getMapBuffer(Key key) const { + // TODO Add checks to verify that offsets are under the boundaries of the map + // buffer + int dynamicDataOffset = getDynamicDataOffset(); + + uint16_t mapBufferLength = 0; + + memcpy( + reinterpret_cast(&mapBufferLength), + reinterpret_cast(_data + dynamicDataOffset), + UINT16_SIZE); + + int valueOffset = getInt(key) + UINT16_SIZE; + + uint8_t *value = new Byte[mapBufferLength]; + + memcpy( + reinterpret_cast(value), + reinterpret_cast( + _data + dynamicDataOffset + valueOffset), + mapBufferLength); + + return MapBuffer(value, mapBufferLength); +} + +bool MapBuffer::isNull(Key key) const { return getInt(key) == NULL_VALUE; } -uint16_t MapBuffer::getBufferSize() { +uint16_t MapBuffer::getBufferSize() const { return _dataSize; } -void MapBuffer::copy(uint8_t *output) { +void MapBuffer::copy(uint8_t *output) const { memcpy(output, _data, _dataSize); } -uint16_t MapBuffer::getSize() { +uint16_t MapBuffer::getCount() const { uint16_t size = 0; + memcpy( reinterpret_cast(&size), reinterpret_cast( _data + UINT16_SIZE), // TODO refactor this: + UINT16_SIZE describes // the position in the header UINT16_SIZE); + return size; } diff --git a/ReactCommon/react/renderer/mapbuffer/MapBuffer.h b/ReactCommon/react/renderer/mapbuffer/MapBuffer.h index 0a775e4c5b25b2..6b79db2943c736 100644 --- a/ReactCommon/react/renderer/mapbuffer/MapBuffer.h +++ b/ReactCommon/react/renderer/mapbuffer/MapBuffer.h @@ -9,12 +9,11 @@ #include +#include + namespace facebook { namespace react { -// 506 = 5 entries = 50*10 + 6 sizeof(header) -constexpr uint16_t INITIAL_SIZE = 506; - /** * MapBuffer is an optimized map format for transferring data like props between * C++ and other platforms The implementation of this map is optimized to: @@ -32,50 +31,42 @@ constexpr uint16_t INITIAL_SIZE = 506; */ class MapBuffer { private: - Header _header = {ALIGNMENT, 0, 0}; - - void makeSpace(); - - void putBytes(Key key, uint8_t *value, int valueSize); - // Buffer and its size - uint8_t *_data; + const uint8_t *_data; + // amount of bytes in the MapBuffer uint16_t _dataSize; - public: - MapBuffer() : MapBuffer(INITIAL_SIZE) {} + // amount of items in the MapBuffer + uint16_t _count; - MapBuffer(uint16_t initialSize); + // returns the relative offset of the first byte of dynamic data + int getDynamicDataOffset() const; - ~MapBuffer(); - - void putInt(Key key, int value); - - void putBool(Key key, bool value); + public: + MapBuffer(uint8_t *const data, uint16_t dataSize); - void putDouble(Key key, double value); + ~MapBuffer(); - void putNull(Key key); + int getInt(Key key) const; - // TODO: create a MapBufferBuilder instead or add checks to verify - // if it's ok to read and write the Map - void finish(); + bool getBool(Key key) const; - int getInt(Key key); + double getDouble(Key key) const; - bool getBool(Key key); + std::string getString(Key key) const; - double getDouble(Key key); + // TODO: review this declaration + MapBuffer getMapBuffer(Key key) const; - uint16_t getBufferSize(); + uint16_t getBufferSize() const; // TODO: review parameters of copy method - void copy(uint8_t *output); + void copy(uint8_t *output) const; - bool isNull(Key key); + bool isNull(Key key) const; - uint16_t getSize(); + uint16_t getCount() const; }; } // namespace react diff --git a/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp b/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp new file mode 100644 index 00000000000000..a4d272cccab63c --- /dev/null +++ b/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.cpp @@ -0,0 +1,212 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "MapBufferBuilder.h" + +using namespace facebook::react; + +namespace facebook { +namespace react { + +MapBufferBuilder::MapBufferBuilder() + : MapBufferBuilder::MapBufferBuilder(INITIAL_KEY_VALUE_SIZE) {} + +MapBuffer MapBufferBuilder::EMPTY() { + static auto emptyMap = MapBufferBuilder().build(); + return emptyMap; +} + +MapBufferBuilder::MapBufferBuilder(uint16_t initialSize) { + _keyValuesSize = initialSize; + _keyValues = new Byte[_keyValuesSize]; + // First Key should be written right after the header. + _keyValuesOffset = HEADER_SIZE; + + _dynamicDataSize = 0; + _dynamicDataValues = nullptr; + _dynamicDataOffset = 0; +} + +void MapBufferBuilder::ensureKeyValueSpace() { + int oldKeyValuesSize = _keyValuesSize; + if (_keyValuesSize >= std::numeric_limits::max() / 2) { + LOG(ERROR) + << "Error: trying to assign a value beyond the capacity of uint16_t" + << static_cast(_keyValuesSize) * 2; + throw "Error: trying to assign a value beyond the capacity of uint16_t"; + } + _keyValuesSize *= 2; + uint8_t *newKeyValues = new Byte[_keyValuesSize]; + uint8_t *oldKeyValues = _keyValues; + memcpy(newKeyValues, _keyValues, oldKeyValuesSize); + _keyValues = newKeyValues; + delete[] oldKeyValues; +} + +void MapBufferBuilder::storeKeyValue(Key key, uint8_t *value, int valueSize) { + if (key < _minKeyToStore) { + LOG(ERROR) << "Error: key out of order - key: " << key; + throw "Error: key out of order"; + } + if (valueSize > MAX_VALUE_SIZE) { + throw "Error: size of value must be <= MAX_VALUE_SIZE"; + } + // TODO: header.count points to the next index + // TODO: add test to verify storage of sparse keys + int keyOffset = getKeyOffset(_header.count); + int valueOffset = keyOffset + KEY_SIZE; + + int nextKeyValueOffset = keyOffset + BUCKET_SIZE; + if (nextKeyValueOffset >= _keyValuesSize) { + ensureKeyValueSpace(); + } + + memcpy(_keyValues + keyOffset, &key, KEY_SIZE); + memcpy(_keyValues + valueOffset, value, valueSize); + + _header.count++; + + _minKeyToStore = key + 1; + // Move _keyValuesOffset to the next available [key, value] position + _keyValuesOffset = std::max(nextKeyValueOffset, _keyValuesOffset); +} + +void MapBufferBuilder::putBool(Key key, bool value) { + putInt(key, (int)value); +} + +void MapBufferBuilder::putDouble(Key key, double value) { + uint8_t *bytePointer = reinterpret_cast(&value); + storeKeyValue(key, bytePointer, DOUBLE_SIZE); +} + +void MapBufferBuilder::putNull(Key key) { + putInt(key, NULL_VALUE); +} + +void MapBufferBuilder::putInt(Key key, int value) { + uint8_t *bytePointer = reinterpret_cast(&(value)); + storeKeyValue(key, bytePointer, INT_SIZE); +} + +void MapBufferBuilder::ensureDynamicDataSpace(int size) { + if (_dynamicDataValues == nullptr) { + _dynamicDataSize = std::max(INITIAL_DYNAMIC_DATA_SIZE, size); + _dynamicDataValues = new Byte[_dynamicDataSize]; + _dynamicDataOffset = 0; + return; + } + + if (_dynamicDataOffset + size >= _dynamicDataSize) { + int oldDynamicDataSize = _dynamicDataSize; + if (_dynamicDataSize >= std::numeric_limits::max() / 2) { + LOG(ERROR) + << "Error: trying to assign a value beyond the capacity of uint16_t" + << static_cast(_dynamicDataSize) * 2; + throw "Error: trying to assign a value beyond the capacity of uint16_t"; + } + _dynamicDataSize *= 2; + uint8_t *newDynamicDataValues = new Byte[_dynamicDataSize]; + uint8_t *oldDynamicDataValues = _dynamicDataValues; + memcpy(newDynamicDataValues, _dynamicDataValues, oldDynamicDataSize); + _dynamicDataValues = newDynamicDataValues; + delete[] oldDynamicDataValues; + } +} + +void MapBufferBuilder::putString(Key key, std::string value) { + int strLength = value.length(); + const char *cstring = getCstring(&value); + + // format [lenght of string (int)] + [Array of Characters in the string] + int sizeOfLength = INT_SIZE; + // TODO : review if map.getBufferSize() should be an int or long instead of an + // int16 (because strings can be longer than int16); + + int sizeOfDynamicData = sizeOfLength + strLength; + ensureDynamicDataSpace(sizeOfDynamicData); + memcpy(_dynamicDataValues + _dynamicDataOffset, &strLength, sizeOfLength); + memcpy( + _dynamicDataValues + _dynamicDataOffset + sizeOfLength, + cstring, + strLength); + + // Store Key and pointer to the string + putInt(key, _dynamicDataOffset); + + _dynamicDataOffset += sizeOfDynamicData; +} + +void MapBufferBuilder::putMapBuffer(Key key, MapBuffer &map) { + uint16_t mapBufferSize = map.getBufferSize(); + + // format [lenght of buffer (short)] + [Array of Characters in the string] + int sizeOfDynamicData = mapBufferSize + UINT16_SIZE; + + // format [Array of bytes of the mapBuffer] + ensureDynamicDataSpace(sizeOfDynamicData); + + memcpy(_dynamicDataValues + _dynamicDataOffset, &mapBufferSize, UINT16_SIZE); + // Copy the content of the map into _dynamicDataValues + map.copy(_dynamicDataValues + _dynamicDataOffset + UINT16_SIZE); + + // Store Key and pointer to the string + putInt(key, _dynamicDataOffset); + + _dynamicDataOffset += sizeOfDynamicData; +} + +MapBuffer MapBufferBuilder::build() { + // Create buffer: [header] + [key, values] + [dynamic data] + int bufferSize = _keyValuesOffset + _dynamicDataOffset; + + _header.bufferSize = bufferSize; + + // Copy header at the beginning of "_keyValues" + memcpy(_keyValues, &_header, HEADER_SIZE); + + uint8_t *buffer = new Byte[bufferSize]; + + memcpy(buffer, _keyValues, _keyValuesOffset); + + if (_dynamicDataValues != nullptr) { + memcpy(buffer + _keyValuesOffset, _dynamicDataValues, _dynamicDataOffset); + } + + // TODO: should we use std::move here? + auto map = MapBuffer(buffer, bufferSize); + + // TODO: we should invalidate the class once the build() method is + // called. + + // Reset internal data + delete[] _keyValues; + _keyValues = nullptr; + _keyValuesSize = 0; + _keyValuesOffset = 0; + + if (_dynamicDataValues != nullptr) { + delete[] _dynamicDataValues; + _dynamicDataValues = nullptr; + } + _dynamicDataSize = 0; + _dynamicDataOffset = 0; + + return map; +} + +MapBufferBuilder::~MapBufferBuilder() { + if (_keyValues != nullptr) { + delete[] _keyValues; + } + if (_dynamicDataValues != nullptr) { + delete[] _dynamicDataValues; + } +} + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.h b/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.h new file mode 100644 index 00000000000000..7222ba2981e71c --- /dev/null +++ b/ReactCommon/react/renderer/mapbuffer/MapBufferBuilder.h @@ -0,0 +1,92 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include +#include + +namespace facebook { +namespace react { + +// Default initial size for _keyValues array +// 106 = 10 entries = 10*10 + 6 sizeof(header) +constexpr uint16_t INITIAL_KEY_VALUE_SIZE = 106; + +// Default initial size for _dynamicDataValues array +constexpr int INITIAL_DYNAMIC_DATA_SIZE = 200; + +/** + * MapBufferBuilder is a builder class for MapBuffer + */ +class MapBufferBuilder { + private: + Header _header = {ALIGNMENT, 0, 0}; + + void ensureKeyValueSpace(); + + void ensureDynamicDataSpace(int size); + + void storeKeyValue(Key key, uint8_t *value, int valueSize); + + // Array of [key,value] map entries: + // - Key is represented in 2 bytes + // - Value is stored into 8 bytes. The 8 bytes of the value will contain the + // actual value for the key or a pointer to the actual value (based on the + // type) + uint8_t *_keyValues; + + // Amount of bytes allocated on _keyValues + uint16_t _keyValuesSize; + + // Relative offset on the _keyValues array. + // This represents the first byte that can be written in _keyValues array + int _keyValuesOffset; + + // This array contains data for dynamic values in the MapBuffer. + // A dynamic value is a String or another MapBuffer. + uint8_t *_dynamicDataValues; + + // Amount of bytes allocated on _dynamicDataValues + uint16_t _dynamicDataSize; + + // Relative offset on the _dynamicDataValues array. + // This represents the first byte that can be written in _dynamicDataValues + // array + int _dynamicDataOffset; + + // Minimmum key to store in the MapBuffer (this is used to guarantee + // consistency) + uint16_t _minKeyToStore = 0; + + public: + MapBufferBuilder(); + + MapBufferBuilder(uint16_t initialSize); + + ~MapBufferBuilder(); + + static MapBuffer EMPTY(); + + void putInt(Key key, int value); + + void putBool(Key key, bool value); + + void putDouble(Key key, double value); + + void putNull(Key key); + + void putString(Key key, std::string value); + + void putMapBuffer(Key key, MapBuffer &map); + + // TODO This should return MapBuffer! + MapBuffer build(); +}; + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/react/renderer/mapbuffer/primitives.h b/ReactCommon/react/renderer/mapbuffer/primitives.h index 7800ac70acc66f..ad153fd6f52647 100644 --- a/ReactCommon/react/renderer/mapbuffer/primitives.h +++ b/ReactCommon/react/renderer/mapbuffer/primitives.h @@ -11,12 +11,12 @@ // TODO: Enable CHECK_CONSISTENCY only in debug mode or test environments (or // just in demand) -#define CHECK_CONSISTENCY 1 +// #define CHECK_CONSISTENCY 1 constexpr static int NULL_VALUE = 0; // Value used to verify if the data is serialized with LittleEndian order -constexpr static int ALIGNMENT = 0xFE; +constexpr static uint16_t ALIGNMENT = 0xFE; using Key = uint16_t; @@ -27,8 +27,8 @@ namespace react { struct Header { uint16_t alignment; // alignment of serialization - uint16_t count; // amount of items - uint16_t bufferSize; // Size of buffer that contains Strings and Objects + uint16_t count; // amount of items in the map + uint16_t bufferSize; // Amount of bytes used to store the map in memory }; constexpr static int KEY_SIZE = sizeof(Key); @@ -38,6 +38,11 @@ constexpr static int DOUBLE_SIZE = sizeof(double); constexpr static int UINT8_SIZE = sizeof(uint8_t); constexpr static int UINT16_SIZE = sizeof(uint16_t); constexpr static int UINT64_SIZE = sizeof(uint64_t); +constexpr static int HEADER_ALIGNMENT_OFFSET = 0; +constexpr static int HEADER_COUNT_OFFSET = UINT16_SIZE; +constexpr static int HEADER_BUFFER_SIZE_OFFSET = UINT16_SIZE * 2; + +constexpr static int MAX_VALUE_SIZE = UINT64_SIZE; // 10 bytes : 2 key + 8 value constexpr static int BUCKET_SIZE = KEY_SIZE + UINT64_SIZE; @@ -56,13 +61,17 @@ inline int getValueOffset(Key key) { return getKeyOffset(key) + KEY_SIZE; } +static inline const char *getCstring(const std::string *str) { + return str ? str->c_str() : ""; +} + inline void checkKeyConsistency(const Header &header, const uint8_t *data, Key key) { #ifdef CHECK_CONSISTENCY if (key >= header.count) { LOG(ERROR) << "Error: Key is higher than size of Map - key '" << key << "' - size: '" << header.count << "'"; - exit(1); + assert(false && "Error while reading key"); } Key storedKey = 0; @@ -74,7 +83,7 @@ checkKeyConsistency(const Header &header, const uint8_t *data, Key key) { if (storedKey != key) { LOG(ERROR) << "Error while reading key, expecting '" << key << "' found: '" << storedKey << "'"; - exit(1); + assert(false && "Error while reading key"); } #endif } diff --git a/ReactCommon/react/renderer/mapbuffer/tests/MapBufferTest.cpp b/ReactCommon/react/renderer/mapbuffer/tests/MapBufferTest.cpp index 5d718d50493b8c..cb18889cf76f7b 100644 --- a/ReactCommon/react/renderer/mapbuffer/tests/MapBufferTest.cpp +++ b/ReactCommon/react/renderer/mapbuffer/tests/MapBufferTest.cpp @@ -9,81 +9,136 @@ #include #include +#include using namespace facebook::react; -TEST(MapBufferTest, testMapGrowth) { - auto buffer = MapBuffer(); +TEST(MapBufferTest, testSimpleIntMap) { + auto builder = MapBufferBuilder(); - buffer.putInt(0, 1234); - buffer.putInt(1, 4321); + builder.putInt(0, 1234); + builder.putInt(1, 4321); - buffer.finish(); + auto map = builder.build(); - EXPECT_EQ(buffer.getSize(), 2); - EXPECT_EQ(buffer.getInt(0), 1234); - EXPECT_EQ(buffer.getInt(1), 4321); + EXPECT_EQ(map.getCount(), 2); + EXPECT_EQ(map.getInt(0), 1234); + EXPECT_EQ(map.getInt(1), 4321); } TEST(MapBufferTest, testMapBufferExtension) { // 26 = 2 buckets: 2*10 + 6 sizeof(header) int initialSize = 26; - auto buffer = MapBuffer(initialSize); + auto buffer = MapBufferBuilder(initialSize); buffer.putInt(0, 1234); buffer.putInt(1, 4321); buffer.putInt(2, 2121); buffer.putInt(3, 1212); - buffer.finish(); + auto map = buffer.build(); - EXPECT_EQ(buffer.getSize(), 4); + EXPECT_EQ(map.getCount(), 4); - EXPECT_EQ(buffer.getInt(0), 1234); - EXPECT_EQ(buffer.getInt(1), 4321); - EXPECT_EQ(buffer.getInt(2), 2121); - EXPECT_EQ(buffer.getInt(3), 1212); + EXPECT_EQ(map.getInt(0), 1234); + EXPECT_EQ(map.getInt(1), 4321); + EXPECT_EQ(map.getInt(2), 2121); + EXPECT_EQ(map.getInt(3), 1212); } TEST(MapBufferTest, testBoolEntries) { - auto buffer = MapBuffer(); + auto buffer = MapBufferBuilder(); buffer.putBool(0, true); buffer.putBool(1, false); - buffer.finish(); + auto map = buffer.build(); - EXPECT_EQ(buffer.getSize(), 2); - EXPECT_EQ(buffer.getBool(0), true); - EXPECT_EQ(buffer.getBool(1), false); + EXPECT_EQ(map.getCount(), 2); + EXPECT_EQ(map.getBool(0), true); + EXPECT_EQ(map.getBool(1), false); } TEST(MapBufferTest, testNullEntries) { - auto buffer = MapBuffer(); + auto buffer = MapBufferBuilder(); buffer.putNull(0); buffer.putInt(1, 1234); - buffer.finish(); + auto map = buffer.build(); - EXPECT_EQ(buffer.getSize(), 2); - EXPECT_EQ(buffer.isNull(0), true); - EXPECT_EQ(buffer.isNull(1), false); + EXPECT_EQ(map.getCount(), 2); + EXPECT_EQ(map.isNull(0), true); + EXPECT_EQ(map.isNull(1), false); // TODO: serialize null values to be distinguishable from '0' values - // EXPECT_EQ(buffer.isNull(1), false); - // EXPECT_EQ(buffer.getBool(1), false); + // EXPECT_EQ(map.isNull(1), false); + // EXPECT_EQ(map.getBool(1), false); } TEST(MapBufferTest, testDoubleEntries) { - auto buffer = MapBuffer(); + auto buffer = MapBufferBuilder(); buffer.putDouble(0, 123.4); buffer.putDouble(1, 432.1); - buffer.finish(); + auto map = buffer.build(); + + EXPECT_EQ(map.getCount(), 2); + + EXPECT_EQ(map.getDouble(0), 123.4); + EXPECT_EQ(map.getDouble(1), 432.1); +} + +TEST(MapBufferTest, testStringEntries) { + auto builder = MapBufferBuilder(); + + builder.putString(0, "This is a test"); + auto map = builder.build(); + + EXPECT_EQ(map.getString(0), "This is a test"); +} + +TEST(MapBufferTest, testUTFStringEntries) { + auto builder = MapBufferBuilder(); + + builder.putString(0, "Let's count: 的, 一, 是"); + auto map = builder.build(); + + EXPECT_EQ(map.getString(0), "Let's count: 的, 一, 是"); +} + +TEST(MapBufferTest, testEmptyMap) { + auto builder = MapBufferBuilder(); + auto map = builder.build(); + EXPECT_EQ(map.getCount(), 0); +} + +TEST(MapBufferTest, testEmptyMapConstant) { + auto map = MapBufferBuilder::EMPTY(); + EXPECT_EQ(map.getCount(), 0); +} + +TEST(MapBufferTest, testMapEntries) { + auto builder = MapBufferBuilder(); + builder.putString(0, "This is a test"); + builder.putInt(1, 1234); + auto map = builder.build(); + + EXPECT_EQ(map.getCount(), 2); + EXPECT_EQ(map.getString(0), "This is a test"); + EXPECT_EQ(map.getInt(1), 1234); + + auto builder2 = MapBufferBuilder(); + builder2.putInt(0, 4321); + builder2.putMapBuffer(1, map); + auto map2 = builder2.build(); + + EXPECT_EQ(map2.getCount(), 2); + EXPECT_EQ(map2.getInt(0), 4321); - EXPECT_EQ(buffer.getSize(), 2); + MapBuffer readMap2 = map2.getMapBuffer(1); - EXPECT_EQ(buffer.getDouble(0), 123.4); - EXPECT_EQ(buffer.getDouble(1), 432.1); + EXPECT_EQ(readMap2.getCount(), 2); + EXPECT_EQ(readMap2.getString(0), "This is a test"); + EXPECT_EQ(readMap2.getInt(1), 1234); } diff --git a/ReactCommon/react/renderer/textlayoutmanager/Android.mk b/ReactCommon/react/renderer/textlayoutmanager/Android.mk index 5af214a5ef70f2..500366152a9953 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/Android.mk +++ b/ReactCommon/react/renderer/textlayoutmanager/Android.mk @@ -11,7 +11,7 @@ LOCAL_MODULE := react_render_textlayoutmanager LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp $(LOCAL_PATH)/platform/android/react/renderer/textlayoutmanager/*.cpp) -LOCAL_SHARED_LIBRARIES := libfolly_futures libreactnativeutilsjni libreact_utils libfb libfbjni libreact_render_uimanager libreact_render_componentregistry libreact_render_attributedstring libreact_render_mounting libfolly_json libyoga libfolly_json libreact_render_core libreact_render_debug libreact_render_graphics libreact_debug +LOCAL_SHARED_LIBRARIES := libfolly_futures libreactnativeutilsjni libreact_utils libfb libfbjni libreact_render_uimanager libreact_render_componentregistry libreact_render_attributedstring libreact_render_mounting libfolly_json libyoga libfolly_json libreact_render_core libreact_render_debug libreact_render_graphics libreact_debug libreact_render_mapbuffer libmapbufferjni LOCAL_STATIC_LIBRARIES := @@ -39,3 +39,4 @@ $(call import-module,react/renderer/graphics) $(call import-module,react/renderer/uimanager) $(call import-module,react/utils) $(call import-module,yogajni) +$(call import-module,react/renderer/mapbuffer) diff --git a/ReactCommon/react/renderer/textlayoutmanager/BUCK b/ReactCommon/react/renderer/textlayoutmanager/BUCK index c79371156a8e12..de04dc7f8eb93a 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/BUCK +++ b/ReactCommon/react/renderer/textlayoutmanager/BUCK @@ -62,6 +62,7 @@ rn_xplat_cxx_library( cxx_tests = [":tests"], fbandroid_deps = [ react_native_target("jni/react/jni:jni"), + react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"), ], fbandroid_exported_headers = subdir_glob( [ diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp b/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp index 04a21772bcf1ef..f0ae38f5820f4f 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.cpp @@ -40,8 +40,11 @@ TextMeasurement TextLayoutManager::measure( telemetry->willMeasureText(); } - auto measurement = - doMeasure(attributedString, paragraphAttributes, layoutConstraints); + auto measurement = mapBufferSerializationEnabled_ + ? doMeasureMapBuffer( + attributedString, paragraphAttributes, layoutConstraints) + : doMeasure( + attributedString, paragraphAttributes, layoutConstraints); if (telemetry) { telemetry->didMeasureText(); @@ -93,6 +96,10 @@ LinesMeasurements TextLayoutManager::measureLines( AttributedString attributedString, ParagraphAttributes paragraphAttributes, Size size) const { + if (mapBufferSerializationEnabled_) { + return measureLinesMapBuffer(attributedString, paragraphAttributes, size); + } + const jni::global_ref &fabricUIManager = contextContainer_->at>("FabricUIManager"); static auto measureLines = @@ -139,6 +146,47 @@ LinesMeasurements TextLayoutManager::measureLines( return lineMeasurements; } +LinesMeasurements TextLayoutManager::measureLinesMapBuffer( + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + Size size) const { + const jni::global_ref &fabricUIManager = + contextContainer_->at>("FabricUIManager"); + static auto measureLines = + jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") + ->getMethod("measureLinesMapBuffer"); + + auto attributedStringMB = + ReadableMapBuffer::createWithContents(toMapBuffer(attributedString)); + auto paragraphAttributesMB = + ReadableMapBuffer::createWithContents(toMapBuffer(paragraphAttributes)); + + auto array = measureLines( + fabricUIManager, + attributedStringMB.get(), + paragraphAttributesMB.get(), + size.width, + size.height); + + auto dynamicArray = cthis(array)->consume(); + LinesMeasurements lineMeasurements; + lineMeasurements.reserve(dynamicArray.size()); + + for (auto const &data : dynamicArray) { + lineMeasurements.push_back(LineMeasurement(data)); + } + + // Explicitly release smart pointers to free up space faster in JNI tables + attributedStringMB.reset(); + paragraphAttributesMB.reset(); + + return lineMeasurements; +} + TextMeasurement TextLayoutManager::doMeasure( AttributedString attributedString, ParagraphAttributes paragraphAttributes, @@ -201,5 +249,67 @@ TextMeasurement TextLayoutManager::doMeasure( return TextMeasurement{size, attachments}; } +TextMeasurement TextLayoutManager::doMeasureMapBuffer( + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + LayoutConstraints layoutConstraints) const { + layoutConstraints.maximumSize.height = std::numeric_limits::infinity(); + + int attachmentsCount = 0; + for (auto fragment : attributedString.getFragments()) { + if (fragment.isAttachment()) { + attachmentsCount++; + } + } + auto env = Environment::current(); + auto attachmentPositions = env->NewFloatArray(attachmentsCount * 2); + + auto minimumSize = layoutConstraints.minimumSize; + auto maximumSize = layoutConstraints.maximumSize; + + auto attributedStringMap = toMapBuffer(attributedString); + auto paragraphAttributesMap = toMapBuffer(paragraphAttributes); + + auto size = measureAndroidComponentMapBuffer( + contextContainer_, + -1, // TODO: we should pass rootTag in + "RCTText", + attributedStringMap, + paragraphAttributesMap, + minimumSize.width, + maximumSize.width, + minimumSize.height, + maximumSize.height, + attachmentPositions); + + jfloat *attachmentData = env->GetFloatArrayElements(attachmentPositions, 0); + + auto attachments = TextMeasurement::Attachments{}; + if (attachmentsCount > 0) { + int attachmentIndex = 0; + for (auto fragment : attributedString.getFragments()) { + if (fragment.isAttachment()) { + float top = attachmentData[attachmentIndex * 2]; + float left = attachmentData[attachmentIndex * 2 + 1]; + float width = fragment.parentShadowView.layoutMetrics.frame.size.width; + float height = + fragment.parentShadowView.layoutMetrics.frame.size.height; + + auto rect = facebook::react::Rect{ + {left, top}, facebook::react::Size{width, height}}; + attachments.push_back(TextMeasurement::Attachment{rect, false}); + attachmentIndex++; + } + } + } + + // Clean up allocated ref + env->ReleaseFloatArrayElements( + attachmentPositions, attachmentData, JNI_ABORT); + env->DeleteLocalRef(attachmentPositions); + + return TextMeasurement{size, attachments}; +} + } // namespace react } // namespace facebook diff --git a/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h b/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h index 681c54e22fb793..19ae11b2a7d62c 100644 --- a/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h +++ b/ReactCommon/react/renderer/textlayoutmanager/platform/android/react/renderer/textlayoutmanager/TextLayoutManager.h @@ -7,6 +7,7 @@ #pragma once +#include #include #include #include @@ -26,7 +27,11 @@ using SharedTextLayoutManager = std::shared_ptr; class TextLayoutManager { public: TextLayoutManager(const ContextContainer::Shared &contextContainer) - : contextContainer_(contextContainer){}; + : contextContainer_(contextContainer) { + static auto value = + contextContainer->at("MapBufferSerializationEnabled"); + mapBufferSerializationEnabled_ = value; + } ~TextLayoutManager(); /* @@ -67,8 +72,19 @@ class TextLayoutManager { ParagraphAttributes paragraphAttributes, LayoutConstraints layoutConstraints) const; + TextMeasurement doMeasureMapBuffer( + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + LayoutConstraints layoutConstraints) const; + + LinesMeasurements measureLinesMapBuffer( + AttributedString attributedString, + ParagraphAttributes paragraphAttributes, + Size size) const; + void *self_; ContextContainer::Shared contextContainer_; + bool mapBufferSerializationEnabled_; TextMeasureCache measureCache_{}; }; diff --git a/ReactCommon/react/utils/Android.mk b/ReactCommon/react/utils/Android.mk index 4e15562a50dcd0..7e23e6eb26a14e 100644 --- a/ReactCommon/react/utils/Android.mk +++ b/ReactCommon/react/utils/Android.mk @@ -20,8 +20,9 @@ LOCAL_CFLAGS := \ LOCAL_CFLAGS += -fexceptions -frtti -std=c++14 -Wall LOCAL_STATIC_LIBRARIES := -LOCAL_SHARED_LIBRARIES := libreact_debug +LOCAL_SHARED_LIBRARIES := libreact_debug libreact_render_mapbuffer include $(BUILD_SHARED_LIBRARY) $(call import-module,react/debug) +$(call import-module,react/renderer/mapbuffer) diff --git a/ReactCommon/react/utils/BUCK b/ReactCommon/react/utils/BUCK index 6e2f7ab2fe9021..6865e9d443afe2 100644 --- a/ReactCommon/react/utils/BUCK +++ b/ReactCommon/react/utils/BUCK @@ -6,6 +6,7 @@ load( "get_apple_compiler_flags", "get_apple_inspector_flags", "get_preprocessor_flags_for_build_mode", + "react_native_target", "react_native_xplat_target", "rn_xplat_cxx_library", "subdir_glob", @@ -39,6 +40,10 @@ rn_xplat_cxx_library( "-std=c++14", "-Wall", ], + fbandroid_deps = [ + react_native_target("java/com/facebook/react/common/mapbuffer/jni:jni"), + react_native_xplat_target("react/renderer/mapbuffer:mapbuffer"), + ], fbobjc_compiler_flags = APPLE_COMPILER_FLAGS, fbobjc_frameworks = ["Foundation"], fbobjc_preprocessor_flags = get_preprocessor_flags_for_build_mode() + get_apple_inspector_flags(), diff --git a/ReactCommon/react/utils/LayoutManager.h b/ReactCommon/react/utils/LayoutManager.h index 9789fc87d997ce..bc901971a3cbb2 100644 --- a/ReactCommon/react/utils/LayoutManager.h +++ b/ReactCommon/react/utils/LayoutManager.h @@ -10,6 +10,11 @@ #include #include #include +#ifdef ANDROID +#include +#include +#include +#endif #include namespace facebook { @@ -88,6 +93,57 @@ Size measureAndroidComponent( return size; } +Size measureAndroidComponentMapBuffer( + const ContextContainer::Shared &contextContainer, + Tag rootTag, + std::string componentName, + MapBuffer &localData, + MapBuffer &props, + float minWidth, + float maxWidth, + float minHeight, + float maxHeight, + jfloatArray attachmentPositions) { + const jni::global_ref &fabricUIManager = + contextContainer->at>("FabricUIManager"); + auto componentNameRef = make_jstring(componentName); + + static auto measure = + jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") + ->getMethod("measureMapBuffer"); + + auto localDataMap = + ReadableMapBuffer::createWithContents(std::move(localData)); + auto propsMap = ReadableMapBuffer::createWithContents(std::move(props)); + + auto size = yogaMeassureToSize(measure( + fabricUIManager, + rootTag, + componentNameRef.get(), + localDataMap.get(), + propsMap.get(), + minWidth, + maxWidth, + minHeight, + maxHeight, + attachmentPositions)); + + // Explicitly release smart pointers to free up space faster in JNI tables + componentNameRef.reset(); + localDataMap.reset(); + propsMap.reset(); + return size; +} + #endif } // namespace react