From 93aa33d82a64f7760e8fcd5414b4c148b1af2636 Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Fri, 14 Feb 2025 19:20:33 -0800 Subject: [PATCH 1/3] Align logic in BaseTextInputShadowNode to determine updateStateIfNeeded with AndroidTextInputShadowNode (#48585) Summary: [Changelog] [Internal] - Align logic in BaseTextInputShadowNode to determine updateStateIfNeeded with AndroidTextInputShadowNode As a preparation for https://github.com/facebook/react-native/pull/48165 this aligns the implementation of those 2 methods Reviewed By: javache Differential Revision: D68004755 --- .../textinput/BaseTextInputShadowNode.h | 6 ++---- .../AndroidTextInputShadowNode.cpp | 16 ++++++++-------- 2 files changed, 10 insertions(+), 12 deletions(-) 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 9bdd9748804ae4..87bf3eba514596 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h @@ -149,19 +149,17 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode< const auto& stateData = BaseShadowNode::getStateData(); const auto& reactTreeAttributedString = getAttributedString(layoutContext); - react_native_assert(textLayoutManager_); if (stateData.reactTreeAttributedString.isContentEqual( reactTreeAttributedString)) { return; } const auto& props = BaseShadowNode::getConcreteProps(); - TextInputState newState( + BaseShadowNode::setStateData(TextInputState{ AttributedStringBox{reactTreeAttributedString}, reactTreeAttributedString, props.paragraphAttributes, - props.mostRecentEventCount); - BaseShadowNode::setStateData(std::move(newState)); + props.mostRecentEventCount}); } /* 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 d0b2b5b0c54afe..671ea8f1c93633 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 @@ -120,19 +120,19 @@ LayoutConstraints AndroidTextInputShadowNode::getTextConstraints( void AndroidTextInputShadowNode::updateStateIfNeeded() { ensureUnsealed(); - + const auto& stateData = getStateData(); auto reactTreeAttributedString = getAttributedString(); - const auto& state = getStateData(); // Tree is often out of sync with the value of the TextInput. // This is by design - don't change the value of the TextInput in the State, // and therefore in Java, unless the tree itself changes. - if (state.reactTreeAttributedString == reactTreeAttributedString) { + if (stateData.reactTreeAttributedString == reactTreeAttributedString) { return; } // If props event counter is less than what we already have in state, skip it - if (getConcreteProps().mostRecentEventCount < state.mostRecentEventCount) { + const auto& props = BaseShadowNode::getConcreteProps(); + if (props.mostRecentEventCount < stateData.mostRecentEventCount) { return; } @@ -141,16 +141,16 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() { // current attributedString unchanged, and pass in zero for the "event count" // so no changes are applied There's no way to prevent a state update from // flowing to Java, so we just ensure it's a noop in those cases. - auto newEventCount = - state.reactTreeAttributedString.isContentEqual(reactTreeAttributedString) + auto newEventCount = stateData.reactTreeAttributedString.isContentEqual( + reactTreeAttributedString) ? 0 - : getConcreteProps().mostRecentEventCount; + : props.mostRecentEventCount; auto newAttributedString = getMostRecentAttributedString(); setStateData(TextInputState{ AttributedStringBox(newAttributedString), reactTreeAttributedString, - getConcreteProps().paragraphAttributes, + props.paragraphAttributes, newEventCount}); } From 7d57303fc56d10af5f616b7cf228e7dbb3e0d8e5 Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Fri, 14 Feb 2025 19:20:33 -0800 Subject: [PATCH 2/3] Align logic in BaseTextInputShadowNode to getAttributedString with AndroidTextInputShadowNode (#48586) Summary: [Changelog] [Internal] - Align logic in BaseTextInputShadowNode to getAttributedString with AndroidTextInputShadowNode As a preparation for https://github.com/facebook/react-native/pull/48165 this aligns the implementation of those 2 methods Differential Revision: D68005037 --- .../textinput/BaseTextInputShadowNode.h | 16 ++++++--- .../AndroidTextInputShadowNode.cpp | 33 ++++++++----------- 2 files changed, 25 insertions(+), 24 deletions(-) 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 87bf3eba514596..d6f391cb82d263 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h @@ -172,15 +172,21 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode< props.getEffectiveTextAttributes(layoutContext.fontSizeMultiplier); AttributedString attributedString; - attributedString.appendFragment(AttributedString::Fragment{ - .string = props.text, - .textAttributes = textAttributes, - .parentShadowView = ShadowView(*this)}); - auto attachments = BaseTextShadowNode::Attachments{}; + // Use BaseTextShadowNode to get attributed string from children BaseTextShadowNode::buildAttributedString( textAttributes, *this, attributedString, attachments); attributedString.setBaseTextAttributes(textAttributes); + + // BaseTextShadowNode only gets children. We must detect and prepend text + // value attributes manually. + if (!props.text.empty()) { + attributedString.appendFragment(AttributedString::Fragment{ + .string = props.text, + .textAttributes = textAttributes, + .parentShadowView = ShadowView(*this)}); + } + return attributedString; } 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 671ea8f1c93633..c68833468691f5 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 @@ -155,34 +155,29 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() { } AttributedString AndroidTextInputShadowNode::getAttributedString() const { - // Use BaseTextShadowNode to get attributed string from children - auto childTextAttributes = TextAttributes::defaultTextAttributes(); - childTextAttributes.apply(getConcreteProps().textAttributes); + const auto& props = BaseShadowNode::getConcreteProps(); + + auto textAttributes = TextAttributes::defaultTextAttributes(); + textAttributes.apply(props.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; + textAttributes.backgroundColor = clearColor(); - auto attributedString = AttributedString{}; + AttributedString attributedString; auto attachments = BaseTextShadowNode::Attachments{}; + // Use BaseTextShadowNode to get attributed string from children BaseTextShadowNode::buildAttributedString( - childTextAttributes, *this, attributedString, attachments); - attributedString.setBaseTextAttributes(childTextAttributes); + textAttributes, *this, attributedString, attachments); + attributedString.setBaseTextAttributes(textAttributes); // 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 (!props.text.empty()) { + attributedString.appendFragment(AttributedString::Fragment{ + .string = props.text, + .textAttributes = textAttributes, + .parentShadowView = ShadowView(*this)}); } return attributedString; From b1119eb516189152c4dfec1646a085ef18e66a1c Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Fri, 14 Feb 2025 19:20:33 -0800 Subject: [PATCH 3/3] Share common ShadowNode functionality in BaseTextInputShadowNode for Android (#48165) Summary: [Changelog] [Internal] - Share common ShadowNode functionality in BaseTextInputShadowNode for Android This change deletes the current Android implementation - but copies over 'relevant' code into the new shared implementation Reviewed By: javache Differential Revision: D66914447 --- .../textinput/BaseTextInputShadowNode.h | 4 +- .../AndroidTextInputShadowNode.cpp | 178 +----------------- .../AndroidTextInputShadowNode.h | 57 +----- 3 files changed, 16 insertions(+), 223 deletions(-) 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 d6f391cb82d263..c97e0e1abb6723 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h @@ -140,11 +140,10 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode< std::shared_ptr textLayoutManager_; - private: /* * Creates a `State` object if needed. */ - void updateStateIfNeeded(const LayoutContext& layoutContext) { + virtual void updateStateIfNeeded(const LayoutContext& layoutContext) { Sealable::ensureUnsealed(); const auto& stateData = BaseShadowNode::getStateData(); const auto& reactTreeAttributedString = getAttributedString(layoutContext); @@ -190,6 +189,7 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode< return attributedString; } + private: /* * Returns an `AttributedStringBox` which represents text content that should * be used for measuring purposes. It might contain actual text value, 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 c68833468691f5..9f20f346093912 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 @@ -7,25 +7,10 @@ #include "AndroidTextInputShadowNode.h" -#include -#include -#include -#include -#include -#include -#include -#include - namespace facebook::react { extern const char AndroidTextInputComponentName[] = "AndroidTextInput"; -void AndroidTextInputShadowNode::setTextLayoutManager( - std::shared_ptr textLayoutManager) { - ensureUnsealed(); - textLayoutManager_ = std::move(textLayoutManager); -} - Size AndroidTextInputShadowNode::measureContent( const LayoutContext& layoutContext, const LayoutConstraints& layoutConstraints) const { @@ -40,88 +25,15 @@ Size AndroidTextInputShadowNode::measureContent( .size; return layoutConstraints.clamp(textSize); } - - // 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 (attributedString.isEmpty()) { - attributedString = getPlaceholderAttributedString(); - } - - if (attributedString.isEmpty() && getStateData().mostRecentEventCount != 0) { - return {.width = 0, .height = 0}; - } - - TextLayoutContext textLayoutContext; - textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor; - auto textSize = textLayoutManager_ - ->measure( - AttributedStringBox{attributedString}, - getConcreteProps().paragraphAttributes, - textLayoutContext, - textConstraints) - .size; - return layoutConstraints.clamp(textSize); -} - -void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) { - updateStateIfNeeded(); - ConcreteViewShadowNode::layout(layoutContext); -} - -Float AndroidTextInputShadowNode::baseline( - const LayoutContext& /*layoutContext*/, - Size size) const { - AttributedString attributedString = getMostRecentAttributedString(); - - if (attributedString.isEmpty()) { - attributedString = getPlaceholderAttributedString(); - } - - // 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); - - AttributedStringBox attributedStringBox{attributedString}; - return textLayoutManager_->baseline( - attributedStringBox, - getConcreteProps().paragraphAttributes, - size) + - top; -} - -LayoutConstraints AndroidTextInputShadowNode::getTextConstraints( - const LayoutConstraints& layoutConstraints) const { - if (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, - }; - } + return BaseTextInputShadowNode::measureContent( + layoutContext, layoutConstraints); } -void AndroidTextInputShadowNode::updateStateIfNeeded() { - ensureUnsealed(); - const auto& stateData = getStateData(); - auto reactTreeAttributedString = getAttributedString(); +void AndroidTextInputShadowNode::updateStateIfNeeded( + const LayoutContext& layoutContext) { + Sealable::ensureUnsealed(); + const auto& stateData = BaseShadowNode::getStateData(); + const auto& reactTreeAttributedString = getAttributedString(layoutContext); // Tree is often out of sync with the value of the TextInput. // This is by design - don't change the value of the TextInput in the State, @@ -145,84 +57,12 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() { reactTreeAttributedString) ? 0 : props.mostRecentEventCount; - auto newAttributedString = getMostRecentAttributedString(); - setStateData(TextInputState{ - AttributedStringBox(newAttributedString), + BaseShadowNode::setStateData(TextInputState{ + AttributedStringBox{reactTreeAttributedString}, reactTreeAttributedString, props.paragraphAttributes, newEventCount}); } -AttributedString AndroidTextInputShadowNode::getAttributedString() const { - const auto& props = BaseShadowNode::getConcreteProps(); - - auto textAttributes = TextAttributes::defaultTextAttributes(); - textAttributes.apply(props.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. - textAttributes.backgroundColor = clearColor(); - - AttributedString attributedString; - auto attachments = BaseTextShadowNode::Attachments{}; - // Use BaseTextShadowNode to get attributed string from children - BaseTextShadowNode::buildAttributedString( - textAttributes, *this, attributedString, attachments); - attributedString.setBaseTextAttributes(textAttributes); - - // BaseTextShadowNode only gets children. We must detect and prepend text - // value attributes manually. - if (!props.text.empty()) { - attributedString.appendFragment(AttributedString::Fragment{ - .string = props.text, - .textAttributes = textAttributes, - .parentShadowView = ShadowView(*this)}); - } - - return attributedString; -} - -AttributedString AndroidTextInputShadowNode::getMostRecentAttributedString() - const { - const auto& state = getStateData(); - - auto reactTreeAttributedString = getAttributedString(); - - // 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); -} - -// 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 { - const auto& props = BaseShadowNode::getConcreteProps(); - - AttributedString attributedString; - auto placeholderString = !props.placeholder.empty() - ? props.placeholder - : BaseTextShadowNode::getEmptyPlaceholder(); - auto textAttributes = TextAttributes::defaultTextAttributes(); - textAttributes.apply(props.textAttributes); - attributedString.appendFragment( - {.string = std::move(placeholderString), - .textAttributes = textAttributes, - .parentShadowView = ShadowView(*this)}); - return attributedString; -} - } // 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 88241d4fcbd062..5896f42be2ec69 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 @@ -10,10 +10,8 @@ #include "AndroidTextInputEventEmitter.h" #include "AndroidTextInputProps.h" -#include +#include #include -#include -#include namespace facebook::react { @@ -23,66 +21,21 @@ extern const char AndroidTextInputComponentName[]; * `ShadowNode` for component. */ class AndroidTextInputShadowNode final - : public ConcreteViewShadowNode< + : public BaseTextInputShadowNode< AndroidTextInputComponentName, AndroidTextInputProps, AndroidTextInputEventEmitter, TextInputState, /* usesMapBufferForStateData */ true> { public: - using ConcreteViewShadowNode::ConcreteViewShadowNode; + using BaseTextInputShadowNode::BaseTextInputShadowNode; - static ShadowNodeTraits BaseTraits() { - auto traits = ConcreteViewShadowNode::BaseTraits(); - traits.set(ShadowNodeTraits::Trait::LeafYogaNode); - traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode); - traits.set(ShadowNodeTraits::Trait::BaselineYogaNode); - return traits; - } - - /* - * Associates a shared TextLayoutManager with the node. - * `TextInputShadowNode` uses the manager to measure text content - * and construct `TextInputState` objects. - */ - void setTextLayoutManager( - std::shared_ptr textLayoutManager); - - 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; - - std::shared_ptr textLayoutManager_; - - /* - * Determines the constraints to use while measure the underlying text - */ - LayoutConstraints getTextConstraints( - const LayoutConstraints& layoutConstraints) const; - - private: - /* - * Creates a `State` object (with `AttributedText` and - * `TextLayoutManager`) if needed. - */ - void updateStateIfNeeded(); - - /* - * 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; + protected: + void updateStateIfNeeded(const LayoutContext& layoutContext) override; }; } // namespace facebook::react