Skip to content

Commit

Permalink
Preparation for Sharing common ShadowNode functionality in BaseTextIn…
Browse files Browse the repository at this point in the history
…putShadowNode for Android

Summary:
As a preperation for #48165 this change aligns the order of methods between:
- BaseTextInputShadowNode.h
- AndroidTextInputShadowNode.h

to make it easier for future changes to look at the delta between both implementations

Differential Revision: D68001423
  • Loading branch information
christophpurrer authored and facebook-github-bot committed Jan 10, 2025
1 parent 9b646c8 commit a5b52f4
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 172 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,32 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode<
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.
Expand Down Expand Up @@ -150,48 +176,22 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode<
AttributedString getAttributedString(
const LayoutContext& layoutContext) const {
const auto& props = BaseShadowNode::getConcreteProps();
auto textAttributes =
const auto textAttributes =
props.getEffectiveTextAttributes(layoutContext.fontSizeMultiplier);
auto attributedString = AttributedString{};

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;
}

/*
* 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,
};
}
}

/*
* Returns an `AttributedStringBox` which represents text content that should
* be used for measuring purposes. It might contain actual text value,
Expand Down Expand Up @@ -232,8 +232,6 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode<
}
return AttributedStringBox{attributedString};
}

std::shared_ptr<const TextLayoutManager> textLayoutManager_;
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -20,92 +20,81 @@ 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;
void AndroidTextInputShadowNode::setTextLayoutManager(
std::shared_ptr<const TextLayoutManager> textLayoutManager) {
ensureUnsealed();
textLayoutManager_ = std::move(textLayoutManager);
}

auto attributedString = AttributedString{};
auto attachments = BaseTextShadowNode::Attachments{};
BaseTextShadowNode::buildAttributedString(
childTextAttributes, *this, attributedString, attachments);
attributedString.setBaseTextAttributes(childTextAttributes);
Size AndroidTextInputShadowNode::measureContent(
const LayoutContext& layoutContext,
const LayoutConstraints& layoutConstraints) const {
auto textConstraints = getTextConstraints(layoutConstraints);

// 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 (getStateData().cachedAttributedStringId != 0) {
auto textSize = textLayoutManager_
->measureCachedSpannableById(
getStateData().cachedAttributedStringId,
getConcreteProps().paragraphAttributes,
textConstraints)
.size;
return layoutConstraints.clamp(textSize);
}

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;
// 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 (fragment.string.empty()) {
fragment.string = BaseTextShadowNode::getEmptyPlaceholder();
if (attributedString.isEmpty()) {
attributedString = getPlaceholderAttributedString();
}

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));
if (attributedString.isEmpty() && getStateData().mostRecentEventCount != 0) {
return {0, 0};
}

return textAttributedString;
TextLayoutContext textLayoutContext;
textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor;
auto textSize = textLayoutManager_
->measure(
AttributedStringBox{attributedString},
getConcreteProps().paragraphAttributes,
textLayoutContext,
textConstraints)
.size;
return layoutConstraints.clamp(textSize);
}

void AndroidTextInputShadowNode::setTextLayoutManager(
std::shared_ptr<const TextLayoutManager> textLayoutManager) {
ensureUnsealed();
textLayoutManager_ = std::move(textLayoutManager);
void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) {
updateStateIfNeeded();
ConcreteViewShadowNode::layout(layoutContext);
}

AttributedString AndroidTextInputShadowNode::getMostRecentAttributedString()
const {
const auto& state = getStateData();
Float AndroidTextInputShadowNode::baseline(
const LayoutContext& layoutContext,
Size size) const {
AttributedString attributedString = getMostRecentAttributedString();

auto reactTreeAttributedString = getAttributedString();
if (attributedString.isEmpty()) {
attributedString = getPlaceholderAttributedString();
}

// 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);
// 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);

return (
!treeAttributedStringChanged ? state.attributedStringBox.getValue()
: reactTreeAttributedString);
AttributedStringBox attributedStringBox{attributedString};
return textLayoutManager_->baseline(
attributedStringBox,
getConcreteProps().paragraphAttributes,
size) +
top;
}

LayoutConstraints AndroidTextInputShadowNode::getTextConstraints(
Expand Down Expand Up @@ -165,77 +154,86 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() {
newEventCount});
}

#pragma mark - LayoutableShadowNode
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;

Size AndroidTextInputShadowNode::measureContent(
const LayoutContext& layoutContext,
const LayoutConstraints& layoutConstraints) const {
auto textConstraints = getTextConstraints(layoutConstraints);
auto attributedString = AttributedString{};
auto attachments = BaseTextShadowNode::Attachments{};
BaseTextShadowNode::buildAttributedString(
childTextAttributes, *this, attributedString, attachments);
attributedString.setBaseTextAttributes(childTextAttributes);

if (getStateData().cachedAttributedStringId != 0) {
auto textSize = textLayoutManager_
->measureCachedSpannableById(
getStateData().cachedAttributedStringId,
getConcreteProps().paragraphAttributes,
textConstraints)
.size;
return layoutConstraints.clamp(textSize);
// 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));
}

// 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();
return attributedString;
}

if (attributedString.isEmpty()) {
attributedString = getPlaceholderAttributedString();
}
AttributedString AndroidTextInputShadowNode::getMostRecentAttributedString()
const {
const auto& state = getStateData();

if (attributedString.isEmpty() && getStateData().mostRecentEventCount != 0) {
return {0, 0};
}
auto reactTreeAttributedString = getAttributedString();

TextLayoutContext textLayoutContext;
textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor;
auto textSize = textLayoutManager_
->measure(
AttributedStringBox{attributedString},
getConcreteProps().paragraphAttributes,
textLayoutContext,
textConstraints)
.size;
return layoutConstraints.clamp(textSize);
// 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);
}

Float AndroidTextInputShadowNode::baseline(
const LayoutContext& layoutContext,
Size size) const {
AttributedString attributedString = getMostRecentAttributedString();
// 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 (attributedString.isEmpty()) {
attributedString = getPlaceholderAttributedString();
if (fragment.string.empty()) {
fragment.string = BaseTextShadowNode::getEmptyPlaceholder();
}

// 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);
auto textAttributes = TextAttributes::defaultTextAttributes();
textAttributes.apply(getConcreteProps().textAttributes);

AttributedStringBox attributedStringBox{attributedString};
return textLayoutManager_->baseline(
attributedStringBox,
getConcreteProps().paragraphAttributes,
size) +
top;
}
// 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));

void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) {
updateStateIfNeeded();
ConcreteViewShadowNode::layout(layoutContext);
return textAttributedString;
}

} // namespace facebook::react
Loading

0 comments on commit a5b52f4

Please sign in to comment.