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 25697da7b59..967189c6556 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 @@ -125,7 +125,8 @@ public class TextLayoutManager { } } - protected static Spannable getOrCreateSpannableForText( + // public because both ReactTextViewManager and ReactTextInputManager need to use this + public static Spannable getOrCreateSpannableForText( Context context, ReadableMap attributedString) { Spannable preparedSpannableText; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java index 30c5f148574..5aa6b6ac4a2 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/views/textinput/ReactTextInputManager.java @@ -7,7 +7,6 @@ package com.facebook.react.views.textinput; -import android.content.Context; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.os.Build; @@ -31,13 +30,16 @@ import com.facebook.react.bridge.JSApplicationIllegalArgumentException; import com.facebook.react.bridge.ReactContext; import com.facebook.react.bridge.ReadableArray; import com.facebook.react.bridge.ReadableMap; +import com.facebook.react.bridge.ReadableNativeMap; import com.facebook.react.bridge.ReadableType; import com.facebook.react.common.MapBuilder; import com.facebook.react.module.annotations.ReactModule; import com.facebook.react.uimanager.BaseViewManager; import com.facebook.react.uimanager.LayoutShadowNode; import com.facebook.react.uimanager.PixelUtil; +import com.facebook.react.uimanager.ReactStylesDiffMap; import com.facebook.react.uimanager.Spacing; +import com.facebook.react.uimanager.StateWrapper; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.UIManagerModule; import com.facebook.react.uimanager.ViewDefaults; @@ -50,10 +52,10 @@ import com.facebook.react.views.scroll.ScrollEvent; import com.facebook.react.views.scroll.ScrollEventType; import com.facebook.react.views.text.DefaultStyleValuesUtil; import com.facebook.react.views.text.ReactTextUpdate; +import com.facebook.react.views.text.TextAttributeProps; import com.facebook.react.views.text.TextInlineImageSpan; import com.facebook.react.views.text.TextLayoutManager; import com.facebook.yoga.YogaConstants; -import com.facebook.yoga.YogaMeasureMode; import java.lang.reflect.Field; import java.util.LinkedList; import java.util.Map; @@ -1024,23 +1026,59 @@ public class ReactTextInputManager extends BaseViewManager #include #include +#include #include #include #include @@ -314,7 +315,8 @@ inline void fromRawValue( } // TODO: remove "underline line-through" after "line-through" deprecation - if (string == "underline-strikethrough" || string == "underline line-through") { + if (string == "underline-strikethrough" || + string == "underline line-through") { result = TextDecorationLineType::UnderlineStrikethrough; return; } @@ -409,6 +411,39 @@ inline std::string toString( } } +inline ParagraphAttributes convertRawProp( + RawProps const &rawProps, + ParagraphAttributes const &defaultParagraphAttributes) { + auto paragraphAttributes = ParagraphAttributes{}; + + paragraphAttributes.maximumNumberOfLines = convertRawProp( + rawProps, + "numberOfLines", + defaultParagraphAttributes.maximumNumberOfLines); + paragraphAttributes.ellipsizeMode = convertRawProp( + rawProps, "ellipsizeMode", defaultParagraphAttributes.ellipsizeMode); + paragraphAttributes.textBreakStrategy = convertRawProp( + rawProps, + "textBreakStrategy", + defaultParagraphAttributes.textBreakStrategy); + paragraphAttributes.adjustsFontSizeToFit = convertRawProp( + rawProps, + "adjustsFontSizeToFit", + defaultParagraphAttributes.adjustsFontSizeToFit); + paragraphAttributes.minimumFontSize = convertRawProp( + rawProps, + "minimumFontSize", + defaultParagraphAttributes.minimumFontSize, + std::numeric_limits::quiet_NaN()); + paragraphAttributes.maximumFontSize = convertRawProp( + rawProps, + "maximumFontSize", + defaultParagraphAttributes.maximumFontSize, + std::numeric_limits::quiet_NaN()); + + return paragraphAttributes; +} + #ifdef ANDROID inline folly::dynamic toDynamic( diff --git a/ReactCommon/fabric/components/text/paragraph/ParagraphProps.cpp b/ReactCommon/fabric/components/text/paragraph/ParagraphProps.cpp index d01de040374..2b9e8926d0e 100644 --- a/ReactCommon/fabric/components/text/paragraph/ParagraphProps.cpp +++ b/ReactCommon/fabric/components/text/paragraph/ParagraphProps.cpp @@ -17,39 +17,6 @@ namespace facebook { namespace react { -static ParagraphAttributes convertRawProp( - RawProps const &rawProps, - ParagraphAttributes const &defaultParagraphAttributes) { - auto paragraphAttributes = ParagraphAttributes{}; - - paragraphAttributes.maximumNumberOfLines = convertRawProp( - rawProps, - "numberOfLines", - defaultParagraphAttributes.maximumNumberOfLines); - paragraphAttributes.ellipsizeMode = convertRawProp( - rawProps, "ellipsizeMode", defaultParagraphAttributes.ellipsizeMode); - paragraphAttributes.textBreakStrategy = convertRawProp( - rawProps, - "textBreakStrategy", - defaultParagraphAttributes.textBreakStrategy); - paragraphAttributes.adjustsFontSizeToFit = convertRawProp( - rawProps, - "adjustsFontSizeToFit", - defaultParagraphAttributes.adjustsFontSizeToFit); - paragraphAttributes.minimumFontSize = convertRawProp( - rawProps, - "minimumFontSize", - defaultParagraphAttributes.minimumFontSize, - std::numeric_limits::quiet_NaN()); - paragraphAttributes.maximumFontSize = convertRawProp( - rawProps, - "maximumFontSize", - defaultParagraphAttributes.maximumFontSize, - std::numeric_limits::quiet_NaN()); - - return paragraphAttributes; -} - ParagraphProps::ParagraphProps( ParagraphProps const &sourceProps, RawProps const &rawProps) diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputComponentDescriptor.h b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputComponentDescriptor.h index 14d0d0d23af..6a1a2c5bfd4 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputComponentDescriptor.h +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputComponentDescriptor.h @@ -26,22 +26,33 @@ class AndroidTextInputComponentDescriptor final : ConcreteComponentDescriptor( eventDispatcher, contextContainer, - flavor) {} + flavor) { + // Every single `AndroidTextInputShadowNode` will have a reference to + // a shared `TextLayoutManager`. + textLayoutManager_ = std::make_shared(contextContainer); + } protected: void adopt(UnsharedShadowNode shadowNode) const override { assert(std::dynamic_pointer_cast(shadowNode)); - auto concreteShadowNode = + auto textInputShadowNode = std::static_pointer_cast(shadowNode); - concreteShadowNode->setContextContainer( + // `ParagraphShadowNode` uses `TextLayoutManager` to measure text content + // and communicate text rendering metrics to mounting layer. + textInputShadowNode->setTextLayoutManager(textLayoutManager_); + + textInputShadowNode->setContextContainer( const_cast(getContextContainer().get())); - concreteShadowNode->dirtyLayout(); - concreteShadowNode->enableMeasurement(); + textInputShadowNode->dirtyLayout(); + textInputShadowNode->enableMeasurement(); ConcreteComponentDescriptor::adopt(shadowNode); } + + private: + SharedTextLayoutManager textLayoutManager_; }; } // namespace react diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputProps.cpp b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputProps.cpp index 13395e9911f..ba4bfe633cb 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputProps.cpp +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputProps.cpp @@ -13,103 +13,11 @@ namespace facebook { namespace react { -/** - * This was cribbed from BaseTextProps. Maybe we can unify someday. - * TODO: we should probably just move this to BaseTextProps / subclass it - * - * @param rawProps - * @param defaultTextAttributes - * @return - */ -static TextAttributes convertRawProp( - const RawProps &rawProps, - const TextAttributes defaultTextAttributes) { - auto textAttributes = TextAttributes{}; - - // Color - textAttributes.foregroundColor = - convertRawProp(rawProps, "color", defaultTextAttributes.foregroundColor); - // Todo T53300333: not found in AndroidTextInput (java) and/or TextInput - textAttributes.backgroundColor = convertRawProp( - rawProps, "backgroundColor", defaultTextAttributes.backgroundColor); - // Todo T53300333: not found in AndroidTextInput (java) and/or TextInput - textAttributes.opacity = - convertRawProp(rawProps, "opacity", defaultTextAttributes.opacity); - - // Font - textAttributes.fontFamily = - convertRawProp(rawProps, "fontFamily", defaultTextAttributes.fontFamily); - textAttributes.fontSize = - convertRawProp(rawProps, "fontSize", defaultTextAttributes.fontSize); - // Todo T53300333: not found in AndroidTextInput (java) and/or TextInput - // is this maxFontSizeMultiplier? - textAttributes.fontSizeMultiplier = convertRawProp( - rawProps, "fontSizeMultiplier", defaultTextAttributes.fontSizeMultiplier); - textAttributes.fontWeight = - convertRawProp(rawProps, "fontWeight", defaultTextAttributes.fontWeight); - textAttributes.fontStyle = - convertRawProp(rawProps, "fontStyle", defaultTextAttributes.fontStyle); - // Todo T53300333: not found in AndroidTextInput (java) and/or TextInput - textAttributes.fontVariant = convertRawProp( - rawProps, "fontVariant", defaultTextAttributes.fontVariant); - textAttributes.allowFontScaling = convertRawProp( - rawProps, "allowFontScaling", defaultTextAttributes.allowFontScaling); - textAttributes.letterSpacing = convertRawProp( - rawProps, "letterSpacing", defaultTextAttributes.letterSpacing); - - // Paragraph - textAttributes.lineHeight = - convertRawProp(rawProps, "lineHeight", defaultTextAttributes.lineHeight); - textAttributes.alignment = - convertRawProp(rawProps, "textAlign", defaultTextAttributes.alignment); - // Todo T53300333: not found in AndroidTextInput (java) and/or TextInput - textAttributes.baseWritingDirection = convertRawProp( - rawProps, - "baseWritingDirection", - defaultTextAttributes.baseWritingDirection); - - // Decoration - // Todo T53300333: not found in AndroidTextInput (java) and/or TextInput - textAttributes.textDecorationColor = convertRawProp( - rawProps, - "textDecorationColor", - defaultTextAttributes.textDecorationColor); - textAttributes.textDecorationLineType = convertRawProp( - rawProps, - "textDecorationLine", - defaultTextAttributes.textDecorationLineType); - // Todo T53300333: not found in AndroidTextInput (java) and/or TextInput - textAttributes.textDecorationLineStyle = convertRawProp( - rawProps, - "textDecorationLineStyle", - defaultTextAttributes.textDecorationLineStyle); - // Todo T53300333: not found in AndroidTextInput (java) and/or TextInput - textAttributes.textDecorationLinePattern = convertRawProp( - rawProps, - "textDecorationLinePattern", - defaultTextAttributes.textDecorationLinePattern); - - // Shadow - textAttributes.textShadowOffset = convertRawProp( - rawProps, "textShadowOffset", defaultTextAttributes.textShadowOffset); - textAttributes.textShadowRadius = convertRawProp( - rawProps, "textShadowRadius", defaultTextAttributes.textShadowRadius); - textAttributes.textShadowColor = convertRawProp( - rawProps, "textShadowColor", defaultTextAttributes.textShadowColor); - - // Special - // Todo T53300333: not found in AndroidTextInput (java) and/or TextInput - textAttributes.isHighlighted = convertRawProp( - rawProps, "isHighlighted", defaultTextAttributes.isHighlighted); - - return textAttributes; -} - AndroidTextInputProps::AndroidTextInputProps( const AndroidTextInputProps &sourceProps, const RawProps &rawProps) : ViewProps(sourceProps, rawProps), - + BaseTextProps(sourceProps, rawProps), autoCompleteType(convertRawProp( rawProps, "autoCompleteType", @@ -313,7 +221,8 @@ AndroidTextInputProps::AndroidTextInputProps( sourceProps.mostRecentEventCount, {0})), text(convertRawProp(rawProps, "text", sourceProps.text, {})), - textAttributes(convertRawProp(rawProps, sourceProps.textAttributes)) {} + paragraphAttributes( + convertRawProp(rawProps, sourceProps.paragraphAttributes)) {} // TODO T53300085: support this in codegen; this was hand-written folly::dynamic AndroidTextInputProps::getDynamic() const { diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputProps.h b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputProps.h index e29eae9502c..deb9014fe8e 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputProps.h +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputProps.h @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -91,7 +92,7 @@ inline folly::dynamic toDynamic(const AndroidTextInputSelectionStruct &value) { } #endif -class AndroidTextInputProps final : public ViewProps { +class AndroidTextInputProps final : public ViewProps, public BaseTextProps { public: AndroidTextInputProps() = default; AndroidTextInputProps( @@ -152,11 +153,11 @@ class AndroidTextInputProps final : public ViewProps { const int mostRecentEventCount{0}; const std::string text{}; - /** - * TextAttributes: see all BaseText. These attributes are not set - * directly; see convertRawProps. + /* + * Contains all prop values that affect visual representation of the + * paragraph. */ - const TextAttributes textAttributes{}; + ParagraphAttributes const paragraphAttributes{}; }; } // namespace react diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp index a25a59ed9a1..7fe415ca24f 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.cpp @@ -39,7 +39,7 @@ AttributedString AndroidTextInputShadowNode::getAttributedString() const { auto const &attributedString = BaseTextShadowNode::getAttributedString(textAttributes, *this); if (!attributedString.isEmpty()) { - return std::move(attributedString); + return attributedString; } } @@ -60,6 +60,32 @@ AttributedString AndroidTextInputShadowNode::getAttributedString() const { return placeholderAttributedString; } +void AndroidTextInputShadowNode::setTextLayoutManager( + SharedTextLayoutManager textLayoutManager) { + ensureUnsealed(); + textLayoutManager_ = textLayoutManager; +} + +void AndroidTextInputShadowNode::updateStateIfNeeded() { + ensureUnsealed(); + + auto attributedString = getAttributedString(); + auto const &state = getStateData(); + + assert(textLayoutManager_); + assert( + (!state.layoutManager || state.layoutManager == textLayoutManager_) && + "`StateData` refers to a different `TextLayoutManager`"); + + if (state.attributedString == attributedString && + state.layoutManager == textLayoutManager_) { + return; + } + + setStateData(AndroidTextInputState{ + attributedString, getProps()->paragraphAttributes, textLayoutManager_}); +} + #pragma mark - LayoutableShadowNode Size AndroidTextInputShadowNode::measure( @@ -70,52 +96,12 @@ Size AndroidTextInputShadowNode::measure( return {0, 0}; } - const jni::global_ref &fabricUIManager = - contextContainer_->at>("FabricUIManager"); - - static auto measure = - jni::findClassStatic("com/facebook/react/fabric/FabricUIManager") - ->getMethod("measure"); - - auto minimumSize = layoutConstraints.minimumSize; - auto maximumSize = layoutConstraints.maximumSize; - - local_ref componentName = - make_jstring(AndroidTextInputComponentName); - - local_ref attributedStringRNM = - ReadableNativeMap::newObjectCxxArgs(toDynamic(attributedString)); - local_ref attributedStringRM = make_local( - reinterpret_cast(attributedStringRNM.get())); - - local_ref nativeLocalProps = make_local( - ReadableNativeMap::createWithContents(getProps()->getDynamic())); - local_ref props = make_local( - reinterpret_cast(nativeLocalProps.get())); - - // For AndroidTextInput purposes: - // localData == textAttributes - return yogaMeassureToSize(measure( - fabricUIManager, - componentName.get(), - attributedStringRM.get(), - props.get(), - nullptr, - minimumSize.width, - maximumSize.width, - minimumSize.height, - maximumSize.height)); + return textLayoutManager_->measure( + attributedString, getProps()->paragraphAttributes, layoutConstraints); } void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) { + updateStateIfNeeded(); ConcreteViewShadowNode::layout(layoutContext); } diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h index 97a5e7ff099..0576be16ca6 100644 --- a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputShadowNode.h @@ -9,6 +9,7 @@ #include "AndroidTextInputEventEmitter.h" #include "AndroidTextInputProps.h" +#include "AndroidTextInputState.h" #include #include @@ -26,7 +27,8 @@ extern const char AndroidTextInputComponentName[]; class AndroidTextInputShadowNode : public ConcreteViewShadowNode< AndroidTextInputComponentName, AndroidTextInputProps, - AndroidTextInputEventEmitter> { + AndroidTextInputEventEmitter, + AndroidTextInputState> { public: using ConcreteViewShadowNode::ConcreteViewShadowNode; @@ -37,6 +39,13 @@ class AndroidTextInputShadowNode : public ConcreteViewShadowNode< */ AttributedString getAttributedString() const; + /* + * Associates a shared TextLayoutManager with the node. + * `ParagraphShadowNode` uses the manager to measure text content + * and construct `ParagraphState` objects. + */ + void setTextLayoutManager(SharedTextLayoutManager textLayoutManager); + #pragma mark - LayoutableShadowNode Size measure(LayoutConstraints layoutConstraints) const override; @@ -44,6 +53,20 @@ class AndroidTextInputShadowNode : public ConcreteViewShadowNode< private: ContextContainer *contextContainer_{}; + + /* + * Creates a `State` object (with `AttributedText` and + * `TextLayoutManager`) if needed. + */ + void updateStateIfNeeded(); + + SharedTextLayoutManager textLayoutManager_; + + /* + * Cached attributed string that represents the content of the subtree started + * from the node. + */ + mutable folly::Optional cachedAttributedString_{}; }; } // namespace react diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp new file mode 100644 index 00000000000..197e9cf9bf7 --- /dev/null +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.cpp @@ -0,0 +1,27 @@ +/* + * 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 "AndroidTextInputState.h" + +#include +#include + +namespace facebook { +namespace react { + +#ifdef ANDROID +folly::dynamic AndroidTextInputState::getDynamic() const { + folly::dynamic newState = folly::dynamic::object(); + newState["attributedString"] = toDynamic(attributedString); + newState["paragraphAttributes"] = toDynamic(paragraphAttributes); + newState["hash"] = newState["attributedString"]["hash"]; + return newState; +} +#endif + +} // namespace react +} // namespace facebook diff --git a/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h new file mode 100644 index 00000000000..05faed89501 --- /dev/null +++ b/ReactCommon/fabric/components/textinput/androidtextinput/AndroidTextInputState.h @@ -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. + */ + +#pragma once + +#include +#include +#include + +#ifdef ANDROID +#include +#endif + +namespace facebook { +namespace react { + +/* + * State for component. + * Represents what to render and how to render. + */ +class AndroidTextInputState final { + public: + /* + * All content of component represented as an `AttributedString`. + */ + AttributedString attributedString; + + /* + * Represents all visual attributes of a paragraph of text represented as + * a ParagraphAttributes. + */ + ParagraphAttributes paragraphAttributes; + + /* + * `TextLayoutManager` provides a connection to platform-specific + * text rendering infrastructure which is capable to render the + * `AttributedString`. + */ + SharedTextLayoutManager layoutManager; + +#ifdef ANDROID + AndroidTextInputState( + AttributedString const &attributedString, + ParagraphAttributes const ¶graphAttributes, + SharedTextLayoutManager const &layoutManager) + : attributedString(attributedString), + paragraphAttributes(paragraphAttributes), + layoutManager(layoutManager) {} + AndroidTextInputState() = default; + AndroidTextInputState(folly::dynamic const &data) { + assert(false && "Not supported"); + }; + folly::dynamic getDynamic() const; +#endif +}; + +} // namespace react +} // namespace facebook