Files
react-native/packages/react-native/ReactCommon/react/renderer/components/textinput/BaseTextInputShadowNode.h
T
Nick Gerleman 2e42eab6df Platform specific TextLayoutManager Headers (#50889)
Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/50889

This effectively reverts D67064488

We are trying to smash together functions, variables, virtual or non-virtual, all required by different platforms, into a single header, often using #ifdefs that are not what we want (apart from a bad editor experience, `#ifdef ANDROID` may or may not be compiled into the react-native-cxx platform, and we cannot use it for the Android platform reliably).

For Facsimile, we are going to be introducing more potential divergence, with the idea of `PreparedText`, where we can generate intermediate products as part of the layout process to be reused later. I'm planning to design that in a way which can be eventually reused across platforms, but not everywhere.

I think the best path for this is going to be to allow each platform to have their own headers, instead of the current messiness, then allow shared code (e.g. in `ParagraphShadowNode`) to pick how to interact at compile time. I added an example of this as part of `TextLayoutManagerExtended`, to customize how we act if a `TextLayoutManager` chooses not to implement `measureLines`.

Changelog: [Internal]

Reviewed By: rshest

Differential Revision: D73557126

fbshipit-source-id: 9851ebba691b0d123f6a355126f2d5b003aceba0
2025-04-29 18:45:58 -07:00

254 lines
9.2 KiB
C++

/*
* 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 <glog/logging.h>
#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/renderer/textlayoutmanager/TextLayoutManagerExtended.h>
#include <react/utils/ContextContainer.h>
namespace facebook::react {
/*
* Base `ShadowNode` for <TextInput> component.
*/
template <
const char* concreteComponentName,
typename ViewPropsT,
typename ViewEventEmitterT,
typename StateDataT>
class BaseTextInputShadowNode : public ConcreteViewShadowNode<
concreteComponentName,
ViewPropsT,
ViewEventEmitterT,
StateDataT>,
public BaseTextShadowNode {
public:
using BaseShadowNode = ConcreteViewShadowNode<
concreteComponentName,
ViewPropsT,
ViewEventEmitterT,
StateDataT>;
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();
auto textConstraints = getTextConstraints(layoutConstraints);
TextLayoutContext textLayoutContext{
.pointScaleFactor = layoutContext.pointScaleFactor};
auto textSize = textLayoutManager_
->measure(
attributedStringBoxToMeasure(layoutContext),
props.paragraphAttributes,
textLayoutContext,
textConstraints)
.size;
return layoutConstraints.clamp(textSize);
}
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()) {
attributedString = getPlaceholderAttributedString(layoutContext);
}
// 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};
if constexpr (TextLayoutManagerExtended::supportsLineMeasurement()) {
auto lines =
TextLayoutManagerExtended(*textLayoutManager_)
.measureLines(
attributedStringBox, props.paragraphAttributes, size);
return LineMeasurement::baseline(lines) + top;
} else {
LOG(WARNING)
<< "Baseline alignment is not supported by the current platform";
return 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<Float>::infinity(),
.height = layoutConstraints.maximumSize.height,
},
.layoutDirection = layoutConstraints.layoutDirection,
};
}
}
std::shared_ptr<const TextLayoutManager> textLayoutManager_;
private:
/*
* Creates a `State` object if needed.
*/
void updateStateIfNeeded(const LayoutContext& layoutContext) {
Sealable::ensureUnsealed();
const auto& stateData = BaseShadowNode::getStateData();
const auto& reactTreeAttributedString = getAttributedString(layoutContext);
if (stateData.reactTreeAttributedString.isContentEqual(
reactTreeAttributedString)) {
return;
}
const auto& props = BaseShadowNode::getConcreteProps();
BaseShadowNode::setStateData(TextInputState{
AttributedStringBox{reactTreeAttributedString},
reactTreeAttributedString,
props.paragraphAttributes,
props.mostRecentEventCount});
}
/*
* Returns a `AttributedString` which represents text content of the node.
*/
AttributedString getAttributedString(
const LayoutContext& layoutContext) const {
const auto& props = BaseShadowNode::getConcreteProps();
const auto textAttributes =
props.getEffectiveTextAttributes(layoutContext.fontSizeMultiplier);
AttributedString attributedString;
attributedString.appendFragment(AttributedString::Fragment{
.string = props.text,
.textAttributes = textAttributes,
.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 &&
BaseShadowNode::getStateData()
.reactTreeAttributedString.getBaseTextAttributes()
.fontSizeMultiplier == layoutContext.fontSizeMultiplier;
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()) {
attributedString = getPlaceholderAttributedString(layoutContext);
}
return AttributedStringBox{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 getPlaceholderAttributedString(
const LayoutContext& layoutContext) const {
const auto& props = BaseShadowNode::getConcreteProps();
AttributedString attributedString;
auto placeholderString = !props.placeholder.empty()
? props.placeholder
: BaseTextShadowNode::getEmptyPlaceholder();
auto textAttributes =
props.getEffectiveTextAttributes(layoutContext.fontSizeMultiplier);
attributedString.appendFragment(
{.string = std::move(placeholderString),
.textAttributes = textAttributes,
.parentShadowView = {}});
return attributedString;
}
};
} // namespace facebook::react