Share common ShadowNode functionality in BaseTextInputShadowNode for iOS (#48164)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/48164

[Changelog] [Internal] - Share common ShadowNode functionality in BaseTextInputShadowNode for iOS

The current Android and iOS implementations have quite some overlapping functionality. Not sharing common logic makes it also harder to reuse this [functionality] for out of tree platforms.

This change moves the current iOS implementation into a shared location.
The next change allows to reuse it for Android.

Reviewed By: NickGerleman

Differential Revision: D66901676

fbshipit-source-id: a870155633875377d881fbd9f41fafb305672949
This commit is contained in:
Christoph Purrer
2024-12-10 18:05:42 -08:00
committed by Facebook GitHub Bot
parent 93c6be496e
commit 7d771de8a7
7 changed files with 253 additions and 231 deletions
@@ -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;
@@ -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
@@ -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 <react/renderer/attributedstring/AttributedString.h>
#include <react/renderer/attributedstring/AttributedStringBox.h>
#include <react/renderer/components/text/BaseTextShadowNode.h>
#include <react/renderer/components/textinput/BaseTextInputProps.h>
#include <react/renderer/components/textinput/TextInputState.h>
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
#include <react/renderer/components/view/YogaLayoutableShadowNode.h>
#include <react/renderer/core/LayoutConstraints.h>
#include <react/renderer/core/LayoutContext.h>
#include <react/renderer/textlayoutmanager/TextLayoutContext.h>
#include <react/renderer/textlayoutmanager/TextLayoutManager.h>
#include <react/utils/ContextContainer.h>
namespace facebook::react {
/*
* Base `ShadowNode` for <TextInput> 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<const TextLayoutManager> 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<const TextLayoutManager> textLayoutManager_;
};
} // namespace facebook::react
@@ -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
@@ -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
@@ -7,149 +7,8 @@
#include "TextInputShadowNode.h"
#include <react/debug/react_native_assert.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/attributedstring/AttributedStringBox.h>
#include <react/renderer/attributedstring/TextAttributes.h>
#include <react/renderer/core/LayoutConstraints.h>
#include <react/renderer/core/LayoutContext.h>
#include <react/renderer/core/conversions.h>
#include <react/renderer/textlayoutmanager/TextLayoutContext.h>
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<const TextLayoutManager> 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
@@ -7,14 +7,10 @@
#pragma once
#include <react/renderer/attributedstring/AttributedString.h>
#include <react/renderer/components/iostextinput/TextInputProps.h>
#include <react/renderer/components/text/BaseTextShadowNode.h>
#include <react/renderer/components/textinput/BaseTextInputShadowNode.h>
#include <react/renderer/components/textinput/TextInputEventEmitter.h>
#include <react/renderer/components/textinput/TextInputState.h>
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
#include <react/renderer/textlayoutmanager/TextLayoutManager.h>
#include <react/utils/ContextContainer.h>
namespace facebook::react {
@@ -23,61 +19,13 @@ extern const char TextInputComponentName[];
/*
* `ShadowNode` for <TextInput> 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<const TextLayoutManager> 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<const TextLayoutManager> textLayoutManager_;
using BaseTextInputShadowNode::BaseTextInputShadowNode;
};
} // namespace facebook::react