From be7b8b7529f9320a040e3f9ab54935c3453ec3b9 Mon Sep 17 00:00:00 2001 From: Christoph Purrer Date: Sat, 7 Dec 2024 08:35:51 -0800 Subject: [PATCH] Share common ShadowNode functionality in BaseTextInputShadowNode for Android (#48165) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/48165 [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 Differential Revision: D66914447 --- .../textinput/BaseTextInputShadowNode.cpp | 48 ++++- .../AndroidTextInputShadowNode.cpp | 188 +----------------- .../AndroidTextInputShadowNode.h | 47 ++--- .../iostextinput/TextInputShadowNode.cpp | 2 - 4 files changed, 64 insertions(+), 221 deletions(-) diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.cpp index 638fc8f6b9debf..4c698f6a8559ae 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.cpp @@ -64,7 +64,6 @@ AttributedString BaseTextInputShadowNode::getAttributedString( attributedString.appendFragment(AttributedString::Fragment{ .string = props.text, .textAttributes = textAttributes, - // TODO: Is this really meant to be by value? .parentShadowView = ShadowView(getShadowNode())}); auto attachments = BaseTextShadowNode::Attachments{}; @@ -86,21 +85,35 @@ std::optional BaseTextInputShadowNode::updateStateIfNeeded( const BaseTextInputProps& props, const TextInputState& state) const { ensureUnsealed(); - auto reactTreeAttributedString = getAttributedString(layoutContext, props); - react_native_assert(textLayoutManager_); - if (state.reactTreeAttributedString.isContentEqual( - reactTreeAttributedString)) { + // 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) { + return std::nullopt; + } + + // If props event counter is less than what we already have in state, skip it + if (props.mostRecentEventCount < state.mostRecentEventCount) { return std::nullopt; } - TextInputState newState; - newState.attributedStringBox = AttributedStringBox{reactTreeAttributedString}; - newState.paragraphAttributes = props.paragraphAttributes; - newState.reactTreeAttributedString = reactTreeAttributedString; - newState.mostRecentEventCount = props.mostRecentEventCount; - return newState; + // Even if we're here and updating state, it may be only to update the layout + // manager If that is the case, make sure we don't update text: pass in the + // 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 the UI, so we just ensure it's a noop in those cases. + auto newEventCount = + state.reactTreeAttributedString.isContentEqual(reactTreeAttributedString) + ? 0 + : props.mostRecentEventCount; + + return TextInputState( + AttributedStringBox{reactTreeAttributedString}, + reactTreeAttributedString, + props.paragraphAttributes, + newEventCount); } Size BaseTextInputShadowNode::measureContent( @@ -108,6 +121,19 @@ Size BaseTextInputShadowNode::measureContent( const LayoutConstraints& layoutConstraints, const BaseTextInputProps& props, const TextInputState& state) const { + // 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. + AttributedStringBox attributedStringBox = + attributedStringBoxToMeasure(layoutContext, props, state); + + if (attributedStringBox.getValue().isEmpty() && + state.mostRecentEventCount != 0) { + return {.width = 0, .height = 0}; + } + TextLayoutContext textLayoutContext{}; textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor; return textLayoutManager_ 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 e1a2c436f38230..5009fa75b4ac85 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,145 +7,10 @@ #include "AndroidTextInputShadowNode.h" -#include -#include -#include -#include -#include -#include -#include -#include - 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; - - auto attributedString = AttributedString{}; - auto attachments = BaseTextShadowNode::Attachments{}; - BaseTextShadowNode::buildAttributedString( - childTextAttributes, *this, attributedString, attachments); - attributedString.setBaseTextAttributes(childTextAttributes); - - // 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)); - } - - 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; - - if (fragment.string.empty()) { - fragment.string = BaseTextShadowNode::getEmptyPlaceholder(); - } - - 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)); - - return textAttributedString; -} - -void AndroidTextInputShadowNode::setTextLayoutManager( - SharedTextLayoutManager textLayoutManager) { - ensureUnsealed(); - textLayoutManager_ = std::move(textLayoutManager); -} - -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); -} - -void AndroidTextInputShadowNode::updateStateIfNeeded() { - ensureUnsealed(); - - 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) { - return; - } - - // If props event counter is less than what we already have in state, skip it - if (getConcreteProps().mostRecentEventCount < state.mostRecentEventCount) { - return; - } - - // Even if we're here and updating state, it may be only to update the layout - // manager If that is the case, make sure we don't update text: pass in the - // 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) - ? 0 - : getConcreteProps().mostRecentEventCount; - auto newAttributedString = getMostRecentAttributedString(); - - setStateData(TextInputState{ - AttributedStringBox(newAttributedString), - reactTreeAttributedString, - getConcreteProps().paragraphAttributes, - newEventCount}); -} - -#pragma mark - LayoutableShadowNode - Size AndroidTextInputShadowNode::measureContent( const LayoutContext& layoutContext, const LayoutConstraints& layoutConstraints) const { @@ -157,58 +22,23 @@ Size AndroidTextInputShadowNode::measureContent( layoutConstraints) .size; } - - // 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 {0, 0}; - } - - TextLayoutContext textLayoutContext; - textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor; - return textLayoutManager_ - ->measure( - AttributedStringBox{attributedString}, - getConcreteProps().paragraphAttributes, - textLayoutContext, - layoutConstraints) - .size; + return BaseTextInputShadowNode::measureContent( + layoutContext, layoutConstraints, getConcreteProps(), getStateData()); } 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; + return BaseTextInputShadowNode::baseline( + layoutContext, size, getConcreteProps(), yogaNode_); } void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) { - updateStateIfNeeded(); + if (auto state = BaseTextInputShadowNode::updateStateIfNeeded( + layoutContext, getConcreteProps(), getStateData()); + state.has_value()) { + setStateData(std::move(state.value())); + } ConcreteViewShadowNode::layout(layoutContext); } 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 222ba23c10cd26..e14996bfeebde4 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 @@ -11,6 +11,7 @@ #include "AndroidTextInputProps.h" #include +#include #include #include #include @@ -28,8 +29,13 @@ class AndroidTextInputShadowNode final AndroidTextInputProps, AndroidTextInputEventEmitter, TextInputState, - /* usesMapBufferForStateData */ true> { + /* usesMapBufferForStateData */ true>, + public BaseTextInputShadowNode { public: + ~AndroidTextInputShadowNode() noexcept override = default; + + using ConcreteViewShadowNode::ConcreteViewShadowNode; + static ShadowNodeTraits BaseTraits() { auto traits = ConcreteViewShadowNode::BaseTraits(); traits.set(ShadowNodeTraits::Trait::LeafYogaNode); @@ -37,43 +43,26 @@ class AndroidTextInputShadowNode final return traits; } - using ConcreteViewShadowNode::ConcreteViewShadowNode; - - /* - * Returns a `AttributedString` which represents text content of the node. - */ - AttributedString getAttributedString() const; - AttributedString getPlaceholderAttributedString() const; + bool hasMeaningfulState() const override { + return getState() && + getState()->getRevision() != State::initialRevisionValue; + } - /* - * Associates a shared TextLayoutManager with the node. - * `TextInputShadowNode` uses the manager to measure text content - * and construct `TextInputState` objects. - */ - void setTextLayoutManager(SharedTextLayoutManager textLayoutManager); + const ShadowNode& getShadowNode() const override { + return *this; + } -#pragma mark - LayoutableShadowNode + void ensureUnsealed() const override { + Sealable::ensureUnsealed(); + } 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; - - /* - * Creates a `State` object (with `AttributedText` and - * `TextLayoutManager`) if needed. - */ - void updateStateIfNeeded(); - - SharedTextLayoutManager textLayoutManager_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.cpp index 6adf2279058ba2..fa73211d1f443b 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.cpp @@ -7,8 +7,6 @@ #include "TextInputShadowNode.h" -#include - namespace facebook::react { extern const char TextInputComponentName[] = "TextInput";