Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Share common ShadowNode functionality in BaseTextInputShadowNode for Android #48165

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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 @@ -118,6 +118,8 @@ class BaseTextInputShadowNode : public ConcreteViewShadowNode<
top;
}

std::shared_ptr<const TextLayoutManager> textLayoutManager_;

private:
/*
* Creates a `State` object if needed.
Expand Down Expand Up @@ -148,21 +150,19 @@ 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;
}

Expand Down Expand Up @@ -206,8 +206,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 @@ -7,145 +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";

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

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

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

return textAttributedString;
}

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

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

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

auto reactTreeAttributedString = getAttributedString();
const auto& state = getStateData();

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

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

// Even if we're here and updating state, it may be only to update the layout
// manager If that is the case, make sure we don't update text: pass in the
// 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)
? 0
: getConcreteProps().mostRecentEventCount;
auto newAttributedString = getMostRecentAttributedString();

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

#pragma mark - LayoutableShadowNode

Size AndroidTextInputShadowNode::measureContent(
const LayoutContext& layoutContext,
const LayoutConstraints& layoutConstraints) const {
Expand All @@ -157,59 +22,8 @@ Size AndroidTextInputShadowNode::measureContent(
layoutConstraints)
.size;
}

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

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

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

void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) {
updateStateIfNeeded();
ConcreteViewShadowNode::layout(layoutContext);
return BaseTextInputShadowNode::measureContent(
layoutContext, layoutConstraints);
}

} // 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,59 +21,20 @@ 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;

static ShadowNodeTraits BaseTraits() {
auto traits = ConcreteViewShadowNode::BaseTraits();
traits.set(ShadowNodeTraits::Trait::LeafYogaNode);
traits.set(ShadowNodeTraits::Trait::MeasurableYogaNode);
traits.set(ShadowNodeTraits::Trait::BaselineYogaNode);
return traits;
}

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

/*
* 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);
using BaseTextInputShadowNode::BaseTextInputShadowNode;

#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:
/**
* Get the most up-to-date attributed string for measurement and State.
*/
AttributedString getMostRecentAttributedString() const;

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

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

} // namespace facebook::react
Loading