Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -140,28 +140,25 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode<

std::shared_ptr<const TextLayoutManager> textLayoutManager_;

private:
/*
* Creates a `State` object if needed.
*/
void updateStateIfNeeded(const LayoutContext& layoutContext) {
virtual 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(
BaseShadowNode::setStateData(TextInputState{
AttributedStringBox{reactTreeAttributedString},
reactTreeAttributedString,
props.paragraphAttributes,
props.mostRecentEventCount);
BaseShadowNode::setStateData(std::move(newState));
props.mostRecentEventCount});
}

/*
Expand All @@ -174,18 +171,25 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode<
props.getEffectiveTextAttributes(layoutContext.fontSizeMultiplier);

AttributedString attributedString;
attributedString.appendFragment(AttributedString::Fragment{
.string = props.text,
.textAttributes = textAttributes,
.parentShadowView = ShadowView(*this)});

auto attachments = BaseTextShadowNode::Attachments{};
// Use BaseTextShadowNode to get attributed string from children
BaseTextShadowNode::buildAttributedString(
textAttributes, *this, attributedString, attachments);
attributedString.setBaseTextAttributes(textAttributes);

// BaseTextShadowNode only gets children. We must detect and prepend text
// value attributes manually.
if (!props.text.empty()) {
attributedString.appendFragment(AttributedString::Fragment{
.string = props.text,
.textAttributes = textAttributes,
.parentShadowView = ShadowView(*this)});
}

return attributedString;
}

private:
/*
* Returns an `AttributedStringBox` which represents text content that should
* be used for measuring purposes. It might contain actual text value,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,10 @@

#include "AndroidTextInputShadowNode.h"

#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/attributedstring/AttributedStringBox.h>
#include <react/renderer/attributedstring/TextAttributes.h>
#include <react/renderer/components/text/BaseTextShadowNode.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 AndroidTextInputComponentName[] = "AndroidTextInput";

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

Size AndroidTextInputShadowNode::measureContent(
const LayoutContext& layoutContext,
const LayoutConstraints& layoutConstraints) const {
Expand All @@ -40,99 +25,26 @@ Size AndroidTextInputShadowNode::measureContent(
.size;
return layoutConstraints.clamp(textSize);
}

// 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 {.width = 0, .height = 0};
}

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

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

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

LayoutConstraints AndroidTextInputShadowNode::getTextConstraints(
const LayoutConstraints& layoutConstraints) const {
if (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,
};
}
return BaseTextInputShadowNode::measureContent(
layoutContext, layoutConstraints);
}

void AndroidTextInputShadowNode::updateStateIfNeeded() {
ensureUnsealed();

auto reactTreeAttributedString = getAttributedString();
const auto& state = getStateData();
void AndroidTextInputShadowNode::updateStateIfNeeded(
const LayoutContext& layoutContext) {
Sealable::ensureUnsealed();
const auto& stateData = BaseShadowNode::getStateData();
const auto& reactTreeAttributedString = getAttributedString(layoutContext);

// 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) {
if (stateData.reactTreeAttributedString == reactTreeAttributedString) {
return;
}

// If props event counter is less than what we already have in state, skip it
if (getConcreteProps().mostRecentEventCount < state.mostRecentEventCount) {
const auto& props = BaseShadowNode::getConcreteProps();
if (props.mostRecentEventCount < stateData.mostRecentEventCount) {
return;
}

Expand All @@ -141,93 +53,16 @@ void AndroidTextInputShadowNode::updateStateIfNeeded() {
// 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)
auto newEventCount = stateData.reactTreeAttributedString.isContentEqual(
reactTreeAttributedString)
? 0
: getConcreteProps().mostRecentEventCount;
auto newAttributedString = getMostRecentAttributedString();
: props.mostRecentEventCount;

setStateData(TextInputState{
AttributedStringBox(newAttributedString),
BaseShadowNode::setStateData(TextInputState{
AttributedStringBox{reactTreeAttributedString},
reactTreeAttributedString,
getConcreteProps().paragraphAttributes,
props.paragraphAttributes,
newEventCount});
}

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

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

// 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 {
const auto& props = BaseShadowNode::getConcreteProps();

AttributedString attributedString;
auto placeholderString = !props.placeholder.empty()
? props.placeholder
: BaseTextShadowNode::getEmptyPlaceholder();
auto textAttributes = TextAttributes::defaultTextAttributes();
textAttributes.apply(props.textAttributes);
attributedString.appendFragment(
{.string = std::move(placeholderString),
.textAttributes = textAttributes,
.parentShadowView = ShadowView(*this)});
return attributedString;
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
#include "AndroidTextInputEventEmitter.h"
#include "AndroidTextInputProps.h"

#include <react/renderer/attributedstring/AttributedString.h>
#include <react/renderer/components/textinput/BaseTextInputShadowNode.h>
#include <react/renderer/components/textinput/TextInputState.h>
#include <react/renderer/components/view/ConcreteViewShadowNode.h>
#include <react/utils/ContextContainer.h>

namespace facebook::react {

Expand All @@ -23,66 +21,21 @@ extern const char AndroidTextInputComponentName[];
* `ShadowNode` for <AndroidTextInput> component.
*/
class AndroidTextInputShadowNode final
: public ConcreteViewShadowNode<
: public BaseTextInputShadowNode<
AndroidTextInputComponentName,
AndroidTextInputProps,
AndroidTextInputEventEmitter,
TextInputState,
/* usesMapBufferForStateData */ true> {
public:
using ConcreteViewShadowNode::ConcreteViewShadowNode;
using BaseTextInputShadowNode::BaseTextInputShadowNode;

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

protected:
Size measureContent(
const LayoutContext& layoutContext,
const LayoutConstraints& layoutConstraints) const override;

void layout(LayoutContext layoutContext) override;

Float baseline(const LayoutContext& layoutContext, Size size) const override;

std::shared_ptr<const TextLayoutManager> textLayoutManager_;

/*
* Determines the constraints to use while measure the underlying text
*/
LayoutConstraints getTextConstraints(
const LayoutConstraints& layoutConstraints) const;

private:
/*
* Creates a `State` object (with `AttributedText` and
* `TextLayoutManager`) if needed.
*/
void updateStateIfNeeded();

/*
* Returns a `AttributedString` which represents text content of the node.
*/
AttributedString getAttributedString() const;

/**
* Get the most up-to-date attributed string for measurement and State.
*/
AttributedString getMostRecentAttributedString() const;

AttributedString getPlaceholderAttributedString() const;
protected:
void updateStateIfNeeded(const LayoutContext& layoutContext) override;
};

} // namespace facebook::react
Loading