diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp index 80cbdc55dddb4f..cec8d245f4fc7e 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.cpp @@ -211,6 +211,33 @@ void BaseTextInputProps::setProp( } } +TextAttributes BaseTextInputProps::getEffectiveTextAttributes( + Float fontSizeMultiplier) const { + auto result = TextAttributes::defaultTextAttributes(); + result.fontSizeMultiplier = fontSizeMultiplier; + result.apply(textAttributes); + + /* + * These props are applied to `View`, therefore they must not be a part of + * base text attributes. + */ + result.backgroundColor = clearColor(); + result.opacity = 1; + + return result; +} + +ParagraphAttributes BaseTextInputProps::getEffectiveParagraphAttributes() + const { + auto result = paragraphAttributes; + + if (!multiline) { + result.maximumNumberOfLines = 1; + } + + return result; +} + SubmitBehavior BaseTextInputProps::getNonDefaultSubmitBehavior() const { if (submitBehavior == SubmitBehavior::Default) { return multiline ? SubmitBehavior::Newline : SubmitBehavior::BlurAndSubmit; diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.h index 36c199b3515a83..37d520f8bb1f89 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputProps.h @@ -31,6 +31,15 @@ class BaseTextInputProps : public ViewProps, public BaseTextProps { const char* propName, const RawValue& value); + SubmitBehavior getNonDefaultSubmitBehavior() const; + + /* + * Accessors + */ + TextAttributes getEffectiveTextAttributes(Float fontSizeMultiplier) const; + + ParagraphAttributes getEffectiveParagraphAttributes() const; + #pragma mark - Props /* @@ -71,8 +80,6 @@ class BaseTextInputProps : public ViewProps, public BaseTextProps { SubmitBehavior submitBehavior{SubmitBehavior::Default}; bool multiline{false}; - - SubmitBehavior getNonDefaultSubmitBehavior() const; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h new file mode 100644 index 00000000000000..615619ded8392f --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h @@ -0,0 +1,213 @@ +/* + * Copyright (c) Meta Platforms, Inc. and 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +/* + * Base `ShadowNode` for component. + */ +template < + const char* concreteComponentName, + typename ViewPropsT, + typename ViewEventEmitterT, + typename StateDataT, + bool usesMapBufferForStateData = false> +class BaseTextInputShadowNode : public ConcreteViewShadowNode< + concreteComponentName, + ViewPropsT, + ViewEventEmitterT, + StateDataT, + usesMapBufferForStateData>, + public BaseTextShadowNode { + public: + using BaseShadowNode = ConcreteViewShadowNode< + concreteComponentName, + ViewPropsT, + ViewEventEmitterT, + StateDataT, + usesMapBufferForStateData>; + + using BaseShadowNode::ConcreteViewShadowNode; + + static ShadowNodeTraits BaseTraits() { + auto traits = BaseShadowNode::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) { + Sealable::ensureUnsealed(); + textLayoutManager_ = std::move(textLayoutManager); + } + + protected: + Size measureContent( + const LayoutContext& layoutContext, + const LayoutConstraints& layoutConstraints) const override { + const auto& props = BaseShadowNode::getConcreteProps(); + TextLayoutContext textLayoutContext{ + .pointScaleFactor = layoutContext.pointScaleFactor}; + return textLayoutManager_ + ->measure( + attributedStringBoxToMeasure(layoutContext), + props.getEffectiveParagraphAttributes(), + textLayoutContext, + layoutConstraints) + .size; + } + + void layout(LayoutContext layoutContext) override { + updateStateIfNeeded(layoutContext); + BaseShadowNode::layout(layoutContext); + } + + Float baseline(const LayoutContext& layoutContext, Size size) const override { + const auto& props = BaseShadowNode::getConcreteProps(); + auto attributedString = getAttributedString(layoutContext); + + if (attributedString.isEmpty()) { + auto placeholderString = !props.placeholder.empty() + ? props.placeholder + : BaseTextShadowNode::getEmptyPlaceholder(); + auto textAttributes = + props.getEffectiveTextAttributes(layoutContext.fontSizeMultiplier); + attributedString.appendFragment( + {std::move(placeholderString), textAttributes, {}}); + } + + // 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( + &(YogaLayoutableShadowNode::yogaNode_), YGEdgeTop) + + YGNodeLayoutGetPadding( + &(YogaLayoutableShadowNode::yogaNode_), YGEdgeTop); + + AttributedStringBox attributedStringBox{attributedString}; + return textLayoutManager_->baseline( + attributedStringBox, + props.getEffectiveParagraphAttributes(), + size) + + top; + } + + private: + /* + * Creates a `State` object if needed. + */ + void updateStateIfNeeded(const LayoutContext& layoutContext) { + Sealable::ensureUnsealed(); + 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( + AttributedStringBox{reactTreeAttributedString}, + reactTreeAttributedString, + props.paragraphAttributes, + props.mostRecentEventCount); + BaseShadowNode::setStateData(std::move(newState)); + } + + /* + * Returns a `AttributedString` which represents text content of the node. + */ + AttributedString getAttributedString( + const LayoutContext& layoutContext) const { + const auto& props = BaseShadowNode::getConcreteProps(); + auto textAttributes = + props.getEffectiveTextAttributes(layoutContext.fontSizeMultiplier); + auto 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; + } + + /* + * Returns an `AttributedStringBox` which represents text content that should + * be used for measuring purposes. It might contain actual text value, + * placeholder value or some character that represents the size of the font. + */ + AttributedStringBox attributedStringBoxToMeasure( + const LayoutContext& layoutContext) const { + bool meaningfulState = BaseShadowNode::getState() && + BaseShadowNode::getState()->getRevision() != + State::initialRevisionValue; + if (meaningfulState) { + const auto& stateData = BaseShadowNode::getStateData(); + auto attributedStringBox = stateData.attributedStringBox; + if (attributedStringBox.getMode() == + AttributedStringBox::Mode::OpaquePointer || + !attributedStringBox.getValue().isEmpty()) { + return stateData.attributedStringBox; + } + } + + auto attributedString = meaningfulState + ? AttributedString{} + : getAttributedString(layoutContext); + + if (attributedString.isEmpty()) { + const auto& props = BaseShadowNode::getConcreteProps(); + auto placeholder = props.placeholder; + // Note: `zero-width space` is insufficient in some cases (e.g. when we + // need to measure the "hight" of the font). + // TODO T67606511: We will redefine the measurement of empty strings as + // part of T67606511 + auto string = !placeholder.empty() + ? placeholder + : BaseTextShadowNode::getEmptyPlaceholder(); + auto textAttributes = + props.getEffectiveTextAttributes(layoutContext.fontSizeMultiplier); + attributedString.appendFragment({string, textAttributes, {}}); + } + return AttributedStringBox{attributedString}; + } + + std::shared_ptr textLayoutManager_; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.cpp b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.cpp index 9d7edc0308576b..4f35234996df10 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.cpp +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.cpp @@ -52,30 +52,4 @@ TextInputProps::TextInputProps( sourceProps.onChangeSync, {})){}; -TextAttributes TextInputProps::getEffectiveTextAttributes( - Float fontSizeMultiplier) const { - auto result = TextAttributes::defaultTextAttributes(); - result.fontSizeMultiplier = fontSizeMultiplier; - result.apply(textAttributes); - - /* - * These props are applied to `View`, therefore they must not be a part of - * base text attributes. - */ - result.backgroundColor = clearColor(); - result.opacity = 1; - - return result; -} - -ParagraphAttributes TextInputProps::getEffectiveParagraphAttributes() const { - auto result = paragraphAttributes; - - if (!multiline) { - result.maximumNumberOfLines = 1; - } - - return result; -} - } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.h index 95caad124b4013..40964f33d171fd 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputProps.h @@ -41,12 +41,6 @@ class TextInputProps final : public BaseTextInputProps { bool onKeyPressSync{false}; bool onChangeSync{false}; - - /* - * Accessors - */ - TextAttributes getEffectiveTextAttributes(Float fontSizeMultiplier) const; - ParagraphAttributes getEffectiveParagraphAttributes() const; }; } // 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 48996487617a96..759b012bcd0c3a 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,149 +7,8 @@ #include "TextInputShadowNode.h" -#include -#include -#include -#include -#include -#include -#include -#include - namespace facebook::react { extern const char TextInputComponentName[] = "TextInput"; -AttributedStringBox TextInputShadowNode::attributedStringBoxToMeasure( - const LayoutContext& layoutContext) const { - bool hasMeaningfulState = - getState() && getState()->getRevision() != State::initialRevisionValue; - - if (hasMeaningfulState) { - auto attributedStringBox = getStateData().attributedStringBox; - if (attributedStringBox.getMode() == - AttributedStringBox::Mode::OpaquePointer || - !attributedStringBox.getValue().isEmpty()) { - return getStateData().attributedStringBox; - } - } - - auto attributedString = hasMeaningfulState - ? AttributedString{} - : getAttributedString(layoutContext); - - if (attributedString.isEmpty()) { - auto placeholder = getConcreteProps().placeholder; - // Note: `zero-width space` is insufficient in some cases (e.g. when we need - // to measure the "hight" of the font). - // TODO T67606511: We will redefine the measurement of empty strings as part - // of T67606511 - auto string = !placeholder.empty() - ? placeholder - : BaseTextShadowNode::getEmptyPlaceholder(); - auto textAttributes = getConcreteProps().getEffectiveTextAttributes( - layoutContext.fontSizeMultiplier); - attributedString.appendFragment({string, textAttributes, {}}); - } - - return AttributedStringBox{attributedString}; -} - -AttributedString TextInputShadowNode::getAttributedString( - const LayoutContext& layoutContext) const { - auto textAttributes = getConcreteProps().getEffectiveTextAttributes( - layoutContext.fontSizeMultiplier); - auto attributedString = AttributedString{}; - - attributedString.appendFragment(AttributedString::Fragment{ - .string = getConcreteProps().text, - .textAttributes = textAttributes, - // TODO: Is this really meant to be by value? - .parentShadowView = ShadowView(*this)}); - - auto attachments = Attachments{}; - BaseTextShadowNode::buildAttributedString( - textAttributes, *this, attributedString, attachments); - attributedString.setBaseTextAttributes(textAttributes); - - return attributedString; -} - -void TextInputShadowNode::setTextLayoutManager( - std::shared_ptr textLayoutManager) { - ensureUnsealed(); - textLayoutManager_ = std::move(textLayoutManager); -} - -void TextInputShadowNode::updateStateIfNeeded( - const LayoutContext& layoutContext) { - ensureUnsealed(); - - auto reactTreeAttributedString = getAttributedString(layoutContext); - const auto& state = getStateData(); - - react_native_assert(textLayoutManager_); - if (state.reactTreeAttributedString.isContentEqual( - reactTreeAttributedString)) { - return; - } - - auto newState = TextInputState{}; - newState.attributedStringBox = AttributedStringBox{reactTreeAttributedString}; - newState.paragraphAttributes = getConcreteProps().paragraphAttributes; - newState.reactTreeAttributedString = reactTreeAttributedString; - newState.mostRecentEventCount = getConcreteProps().mostRecentEventCount; - setStateData(std::move(newState)); -} - -#pragma mark - LayoutableShadowNode - -Size TextInputShadowNode::measureContent( - const LayoutContext& layoutContext, - const LayoutConstraints& layoutConstraints) const { - TextLayoutContext textLayoutContext{}; - textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor; - return textLayoutManager_ - ->measure( - attributedStringBoxToMeasure(layoutContext), - getConcreteProps().getEffectiveParagraphAttributes(), - textLayoutContext, - layoutConstraints) - .size; -} - -Float TextInputShadowNode::baseline( - const LayoutContext& layoutContext, - Size size) const { - auto attributedString = getAttributedString(layoutContext); - - if (attributedString.isEmpty()) { - auto placeholderString = !getConcreteProps().placeholder.empty() - ? getConcreteProps().placeholder - : BaseTextShadowNode::getEmptyPlaceholder(); - auto textAttributes = getConcreteProps().getEffectiveTextAttributes( - layoutContext.fontSizeMultiplier); - attributedString.appendFragment( - {std::move(placeholderString), textAttributes, {}}); - } - - // 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{std::move(attributedString)}; - return textLayoutManager_->baseline( - attributedStringBox, - getConcreteProps().getEffectiveParagraphAttributes(), - size) + - top; -} - -void TextInputShadowNode::layout(LayoutContext layoutContext) { - updateStateIfNeeded(layoutContext); - ConcreteViewShadowNode::layout(layoutContext); -} - } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.h b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.h index be483f903b40ee..af9524b605feaa 100644 --- a/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/components/textinput/platform/ios/react/renderer/components/iostextinput/TextInputShadowNode.h @@ -7,14 +7,10 @@ #pragma once -#include #include -#include +#include #include #include -#include -#include -#include namespace facebook::react { @@ -23,61 +19,13 @@ extern const char TextInputComponentName[]; /* * `ShadowNode` for component. */ -class TextInputShadowNode final : public ConcreteViewShadowNode< +class TextInputShadowNode final : public BaseTextInputShadowNode< TextInputComponentName, TextInputProps, TextInputEventEmitter, - TextInputState>, - public BaseTextShadowNode { + TextInputState> { public: - using ConcreteViewShadowNode::ConcreteViewShadowNode; - - 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); - -#pragma mark - LayoutableShadowNode - - 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: - /* - * Creates a `State` object if needed. - */ - void updateStateIfNeeded(const LayoutContext& layoutContext); - - /* - * Returns a `AttributedString` which represents text content of the node. - */ - AttributedString getAttributedString( - const LayoutContext& layoutContext) const; - - /* - * Returns an `AttributedStringBox` which represents text content that should - * be used for measuring purposes. It might contain actual text value, - * placeholder value or some character that represents the size of the font. - */ - AttributedStringBox attributedStringBoxToMeasure( - const LayoutContext& layoutContext) const; - - std::shared_ptr textLayoutManager_; + using BaseTextInputShadowNode::BaseTextInputShadowNode; }; } // namespace facebook::react