diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h index 908b0be8bc2a76..2aabb63ae7d1c0 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h @@ -120,6 +120,32 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode< top; } + /* + * Determines the constraints to use while measure the underlying text + */ + LayoutConstraints getTextConstraints( + const LayoutConstraints& layoutConstraints) const { + if (BaseShadowNode::getConcreteProps().multiline) { + return layoutConstraints; + } else { + // A single line TextInput acts as a horizontal scroller of infinitely + // expandable text, so we want to measure the text as if it is allowed to + // infinitely expand horizontally, and later clamp to the constraints of + // the input. + return LayoutConstraints{ + .minimumSize = layoutConstraints.minimumSize, + .maximumSize = + Size{ + .width = std::numeric_limits::infinity(), + .height = layoutConstraints.maximumSize.height, + }, + .layoutDirection = layoutConstraints.layoutDirection, + }; + } + } + + std::shared_ptr textLayoutManager_; + private: /* * Creates a `State` object if needed. @@ -150,48 +176,22 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode< AttributedString getAttributedString( const LayoutContext& layoutContext) const { const auto& props = BaseShadowNode::getConcreteProps(); - auto textAttributes = + const auto textAttributes = props.getEffectiveTextAttributes(layoutContext.fontSizeMultiplier); - auto attributedString = AttributedString{}; + AttributedString attributedString; attributedString.appendFragment(AttributedString::Fragment{ .string = props.text, .textAttributes = textAttributes, - // TODO: Is this really meant to be by value? .parentShadowView = ShadowView(*this)}); auto attachments = BaseTextShadowNode::Attachments{}; BaseTextShadowNode::buildAttributedString( textAttributes, *this, attributedString, attachments); attributedString.setBaseTextAttributes(textAttributes); - return attributedString; } - /* - * Determines the constraints to use while measure the underlying text - */ - LayoutConstraints getTextConstraints( - const LayoutConstraints& layoutConstraints) const { - if (BaseShadowNode::getConcreteProps().multiline) { - return layoutConstraints; - } else { - // A single line TextInput acts as a horizontal scroller of infinitely - // expandable text, so we want to measure the text as if it is allowed to - // infinitely expand horizontally, and later clamp to the constraints of - // the input. - return LayoutConstraints{ - .minimumSize = layoutConstraints.minimumSize, - .maximumSize = - Size{ - .width = std::numeric_limits::infinity(), - .height = layoutConstraints.maximumSize.height, - }, - .layoutDirection = layoutConstraints.layoutDirection, - }; - } - } - /* * Returns an `AttributedStringBox` which represents text content that should * be used for measuring purposes. It might contain actual text value, @@ -232,8 +232,6 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode< } return AttributedStringBox{attributedString}; } - - std::shared_ptr textLayoutManager_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp index 1c58eba96d529f..f770ce314afd01 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.cpp @@ -20,92 +20,81 @@ namespace facebook::react { extern const char AndroidTextInputComponentName[] = "AndroidTextInput"; -AttributedString AndroidTextInputShadowNode::getAttributedString() const { - // Use BaseTextShadowNode to get attributed string from children - auto childTextAttributes = TextAttributes::defaultTextAttributes(); - childTextAttributes.apply(getConcreteProps().textAttributes); - // Don't propagate the background color of the TextInput onto the attributed - // string. Android tries to render shadow of the background alongside the - // shadow of the text which results in weird artifacts. - childTextAttributes.backgroundColor = HostPlatformColor::UndefinedColor; +void AndroidTextInputShadowNode::setTextLayoutManager( + std::shared_ptr textLayoutManager) { + ensureUnsealed(); + textLayoutManager_ = std::move(textLayoutManager); +} - auto attributedString = AttributedString{}; - auto attachments = BaseTextShadowNode::Attachments{}; - BaseTextShadowNode::buildAttributedString( - childTextAttributes, *this, attributedString, attachments); - attributedString.setBaseTextAttributes(childTextAttributes); +Size AndroidTextInputShadowNode::measureContent( + const LayoutContext& layoutContext, + const LayoutConstraints& layoutConstraints) const { + auto textConstraints = getTextConstraints(layoutConstraints); - // BaseTextShadowNode only gets children. We must detect and prepend text - // value attributes manually. - if (!getConcreteProps().text.empty()) { - auto textAttributes = TextAttributes::defaultTextAttributes(); - textAttributes.apply(getConcreteProps().textAttributes); - auto fragment = AttributedString::Fragment{}; - fragment.string = getConcreteProps().text; - fragment.textAttributes = textAttributes; - // If the TextInput opacity is 0 < n < 1, the opacity of the TextInput and - // text value's background will stack. This is a hack/workaround to prevent - // that effect. - fragment.textAttributes.backgroundColor = clearColor(); - fragment.parentShadowView = ShadowView(*this); - attributedString.prependFragment(std::move(fragment)); + if (getStateData().cachedAttributedStringId != 0) { + auto textSize = textLayoutManager_ + ->measureCachedSpannableById( + getStateData().cachedAttributedStringId, + getConcreteProps().paragraphAttributes, + textConstraints) + .size; + return layoutConstraints.clamp(textSize); } - return attributedString; -} - -// For measurement purposes, we want to make sure that there's at least a -// single character in the string so that the measured height is greater -// than zero. Otherwise, empty TextInputs with no placeholder don't -// display at all. -// TODO T67606511: We will redefine the measurement of empty strings as part -// of T67606511 -AttributedString AndroidTextInputShadowNode::getPlaceholderAttributedString() - const { - // Return placeholder text, since text and children are empty. - auto textAttributedString = AttributedString{}; - auto fragment = AttributedString::Fragment{}; - fragment.string = getConcreteProps().placeholder; + // Layout is called right after measure. + // Measure is marked as `const`, and `layout` is not; so State can be + // updated during layout, but not during `measure`. If State is out-of-date + // in layout, it's too late: measure will have already operated on old + // State. Thus, we use the same value here that we *will* use in layout to + // update the state. + AttributedString attributedString = getMostRecentAttributedString(); - if (fragment.string.empty()) { - fragment.string = BaseTextShadowNode::getEmptyPlaceholder(); + if (attributedString.isEmpty()) { + attributedString = getPlaceholderAttributedString(); } - auto textAttributes = TextAttributes::defaultTextAttributes(); - textAttributes.apply(getConcreteProps().textAttributes); - - // If there's no text, it's possible that this Fragment isn't actually - // appended to the AttributedString (see implementation of appendFragment) - fragment.textAttributes = textAttributes; - fragment.parentShadowView = ShadowView(*this); - textAttributedString.appendFragment(std::move(fragment)); + if (attributedString.isEmpty() && getStateData().mostRecentEventCount != 0) { + return {0, 0}; + } - return textAttributedString; + TextLayoutContext textLayoutContext; + textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor; + auto textSize = textLayoutManager_ + ->measure( + AttributedStringBox{attributedString}, + getConcreteProps().paragraphAttributes, + textLayoutContext, + textConstraints) + .size; + return layoutConstraints.clamp(textSize); } -void AndroidTextInputShadowNode::setTextLayoutManager( - std::shared_ptr textLayoutManager) { - ensureUnsealed(); - textLayoutManager_ = std::move(textLayoutManager); +void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) { + updateStateIfNeeded(); + ConcreteViewShadowNode::layout(layoutContext); } -AttributedString AndroidTextInputShadowNode::getMostRecentAttributedString() - const { - const auto& state = getStateData(); +Float AndroidTextInputShadowNode::baseline( + const LayoutContext& layoutContext, + Size size) const { + AttributedString attributedString = getMostRecentAttributedString(); - auto reactTreeAttributedString = getAttributedString(); + if (attributedString.isEmpty()) { + attributedString = getPlaceholderAttributedString(); + } - // Sometimes the treeAttributedString will only differ from the state - // not by inherent properties (string or prop attributes), but by the frame of - // the parent which has changed Thus, we can't directly compare the entire - // AttributedString - bool treeAttributedStringChanged = - !state.reactTreeAttributedString.compareTextAttributesWithoutFrame( - reactTreeAttributedString); + // Yoga expects a baseline relative to the Node's border-box edge instead of + // the content, so we need to adjust by the padding and border widths, which + // have already been set by the time of baseline alignment + auto top = YGNodeLayoutGetBorder(&yogaNode_, YGEdgeTop) + + YGNodeLayoutGetPadding(&yogaNode_, YGEdgeTop); - return ( - !treeAttributedStringChanged ? state.attributedStringBox.getValue() - : reactTreeAttributedString); + AttributedStringBox attributedStringBox{attributedString}; + return textLayoutManager_->baseline( + attributedStringBox, + getConcreteProps().paragraphAttributes, + size) + + top; } LayoutConstraints AndroidTextInputShadowNode::getTextConstraints( @@ -165,77 +154,86 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() { newEventCount}); } -#pragma mark - LayoutableShadowNode +AttributedString AndroidTextInputShadowNode::getAttributedString() const { + // Use BaseTextShadowNode to get attributed string from children + auto childTextAttributes = TextAttributes::defaultTextAttributes(); + childTextAttributes.apply(getConcreteProps().textAttributes); + // Don't propagate the background color of the TextInput onto the attributed + // string. Android tries to render shadow of the background alongside the + // shadow of the text which results in weird artifacts. + childTextAttributes.backgroundColor = HostPlatformColor::UndefinedColor; -Size AndroidTextInputShadowNode::measureContent( - const LayoutContext& layoutContext, - const LayoutConstraints& layoutConstraints) const { - auto textConstraints = getTextConstraints(layoutConstraints); + auto attributedString = AttributedString{}; + auto attachments = BaseTextShadowNode::Attachments{}; + BaseTextShadowNode::buildAttributedString( + childTextAttributes, *this, attributedString, attachments); + attributedString.setBaseTextAttributes(childTextAttributes); - if (getStateData().cachedAttributedStringId != 0) { - auto textSize = textLayoutManager_ - ->measureCachedSpannableById( - getStateData().cachedAttributedStringId, - getConcreteProps().paragraphAttributes, - textConstraints) - .size; - return layoutConstraints.clamp(textSize); + // BaseTextShadowNode only gets children. We must detect and prepend text + // value attributes manually. + if (!getConcreteProps().text.empty()) { + auto textAttributes = TextAttributes::defaultTextAttributes(); + textAttributes.apply(getConcreteProps().textAttributes); + auto fragment = AttributedString::Fragment{}; + fragment.string = getConcreteProps().text; + fragment.textAttributes = textAttributes; + // If the TextInput opacity is 0 < n < 1, the opacity of the TextInput and + // text value's background will stack. This is a hack/workaround to prevent + // that effect. + fragment.textAttributes.backgroundColor = clearColor(); + fragment.parentShadowView = ShadowView(*this); + attributedString.prependFragment(std::move(fragment)); } - // Layout is called right after measure. - // Measure is marked as `const`, and `layout` is not; so State can be - // updated during layout, but not during `measure`. If State is out-of-date - // in layout, it's too late: measure will have already operated on old - // State. Thus, we use the same value here that we *will* use in layout to - // update the state. - AttributedString attributedString = getMostRecentAttributedString(); + return attributedString; +} - if (attributedString.isEmpty()) { - attributedString = getPlaceholderAttributedString(); - } +AttributedString AndroidTextInputShadowNode::getMostRecentAttributedString() + const { + const auto& state = getStateData(); - if (attributedString.isEmpty() && getStateData().mostRecentEventCount != 0) { - return {0, 0}; - } + auto reactTreeAttributedString = getAttributedString(); - TextLayoutContext textLayoutContext; - textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor; - auto textSize = textLayoutManager_ - ->measure( - AttributedStringBox{attributedString}, - getConcreteProps().paragraphAttributes, - textLayoutContext, - textConstraints) - .size; - return layoutConstraints.clamp(textSize); + // Sometimes the treeAttributedString will only differ from the state + // not by inherent properties (string or prop attributes), but by the frame of + // the parent which has changed Thus, we can't directly compare the entire + // AttributedString + bool treeAttributedStringChanged = + !state.reactTreeAttributedString.compareTextAttributesWithoutFrame( + reactTreeAttributedString); + + return ( + !treeAttributedStringChanged ? state.attributedStringBox.getValue() + : reactTreeAttributedString); } -Float AndroidTextInputShadowNode::baseline( - const LayoutContext& layoutContext, - Size size) const { - AttributedString attributedString = getMostRecentAttributedString(); +// For measurement purposes, we want to make sure that there's at least a +// single character in the string so that the measured height is greater +// than zero. Otherwise, empty TextInputs with no placeholder don't +// display at all. +// TODO T67606511: We will redefine the measurement of empty strings as part +// of T67606511 +AttributedString AndroidTextInputShadowNode::getPlaceholderAttributedString() + const { + // Return placeholder text, since text and children are empty. + auto textAttributedString = AttributedString{}; + auto fragment = AttributedString::Fragment{}; + fragment.string = getConcreteProps().placeholder; - if (attributedString.isEmpty()) { - attributedString = getPlaceholderAttributedString(); + if (fragment.string.empty()) { + fragment.string = BaseTextShadowNode::getEmptyPlaceholder(); } - // Yoga expects a baseline relative to the Node's border-box edge instead of - // the content, so we need to adjust by the padding and border widths, which - // have already been set by the time of baseline alignment - auto top = YGNodeLayoutGetBorder(&yogaNode_, YGEdgeTop) + - YGNodeLayoutGetPadding(&yogaNode_, YGEdgeTop); + auto textAttributes = TextAttributes::defaultTextAttributes(); + textAttributes.apply(getConcreteProps().textAttributes); - AttributedStringBox attributedStringBox{attributedString}; - return textLayoutManager_->baseline( - attributedStringBox, - getConcreteProps().paragraphAttributes, - size) + - top; -} + // If there's no text, it's possible that this Fragment isn't actually + // appended to the AttributedString (see implementation of appendFragment) + fragment.textAttributes = textAttributes; + fragment.parentShadowView = ShadowView(*this); + textAttributedString.appendFragment(std::move(fragment)); -void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) { - updateStateIfNeeded(); - ConcreteViewShadowNode::layout(layoutContext); + return textAttributedString; } } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.h index 1ce5dc9993f6c4..88241d4fcbd062 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/android/react/renderer/components/androidtextinput/AndroidTextInputShadowNode.h @@ -40,12 +40,6 @@ class AndroidTextInputShadowNode final return traits; } - /* - * Returns a `AttributedString` which represents text content of the node. - */ - AttributedString getAttributedString() const; - AttributedString getPlaceholderAttributedString() const; - /* * Associates a shared TextLayoutManager with the node. * `TextInputShadowNode` uses the manager to measure text content @@ -54,20 +48,16 @@ class AndroidTextInputShadowNode final void setTextLayoutManager( std::shared_ptr textLayoutManager); -#pragma mark - LayoutableShadowNode - + protected: Size measureContent( const LayoutContext& layoutContext, const LayoutConstraints& layoutConstraints) const override; + void layout(LayoutContext layoutContext) override; Float baseline(const LayoutContext& layoutContext, Size size) const override; - private: - /** - * Get the most up-to-date attributed string for measurement and State. - */ - AttributedString getMostRecentAttributedString() const; + std::shared_ptr textLayoutManager_; /* * Determines the constraints to use while measure the underlying text @@ -75,13 +65,24 @@ class AndroidTextInputShadowNode final LayoutConstraints getTextConstraints( const LayoutConstraints& layoutConstraints) const; + private: /* * Creates a `State` object (with `AttributedText` and * `TextLayoutManager`) if needed. */ void updateStateIfNeeded(); - std::shared_ptr textLayoutManager_; + /* + * Returns a `AttributedString` which represents text content of the node. + */ + AttributedString getAttributedString() const; + + /** + * Get the most up-to-date attributed string for measurement and State. + */ + AttributedString getMostRecentAttributedString() const; + + AttributedString getPlaceholderAttributedString() const; }; } // namespace facebook::react