Skip to content

Commit

Permalink
Share common ShadowNode functionality in BaseTextInputShadowNode for …
Browse files Browse the repository at this point in the history
…Android (facebook#48165)

Summary:
Pull Request resolved: facebook#48165

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

This change deletes the current Android implementation - but copies over 'relevant' code into the new shared implementation

Differential Revision: D66914447
  • Loading branch information
christophpurrer authored and facebook-github-bot committed Dec 8, 2024
1 parent 49738c8 commit 6fdc784
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 221 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,6 @@ AttributedString BaseTextInputShadowNode::getAttributedString(
attributedString.appendFragment(AttributedString::Fragment{
.string = props.text,
.textAttributes = textAttributes,
// TODO: Is this really meant to be by value?
.parentShadowView = ShadowView(getShadowNode())});

auto attachments = BaseTextShadowNode::Attachments{};
Expand All @@ -86,28 +85,55 @@ std::optional<TextInputState> BaseTextInputShadowNode::updateStateIfNeeded(
const BaseTextInputProps& props,
const TextInputState& state) const {
ensureUnsealed();

auto reactTreeAttributedString = getAttributedString(layoutContext, props);

react_native_assert(textLayoutManager_);
if (state.reactTreeAttributedString.isContentEqual(
reactTreeAttributedString)) {
// 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 std::nullopt;
}

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

TextInputState newState;
newState.attributedStringBox = AttributedStringBox{reactTreeAttributedString};
newState.paragraphAttributes = props.paragraphAttributes;
newState.reactTreeAttributedString = reactTreeAttributedString;
newState.mostRecentEventCount = props.mostRecentEventCount;
return newState;
// 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 the UI, so we just ensure it's a noop in those cases.
auto newEventCount =
state.reactTreeAttributedString.isContentEqual(reactTreeAttributedString)
? 0
: props.mostRecentEventCount;

return TextInputState(
AttributedStringBox{reactTreeAttributedString},
reactTreeAttributedString,
props.paragraphAttributes,
newEventCount);
}

Size BaseTextInputShadowNode::measureContent(
const LayoutContext& layoutContext,
const LayoutConstraints& layoutConstraints,
const BaseTextInputProps& props,
const TextInputState& state) const {
// 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.
AttributedStringBox attributedStringBox =
attributedStringBoxToMeasure(layoutContext, props, state);

if (attributedStringBox.getValue().isEmpty() &&
state.mostRecentEventCount != 0) {
return {.width = 0, .height = 0};
}

TextLayoutContext textLayoutContext{};
textLayoutContext.pointScaleFactor = layoutContext.pointScaleFactor;
return textLayoutManager_
Expand Down
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(
SharedTextLayoutManager 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,58 +22,23 @@ 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;
return BaseTextInputShadowNode::measureContent(
layoutContext, layoutConstraints, getConcreteProps(), getStateData());
}

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;
return BaseTextInputShadowNode::baseline(
layoutContext, size, getConcreteProps(), yogaNode_);
}

void AndroidTextInputShadowNode::layout(LayoutContext layoutContext) {
updateStateIfNeeded();
if (auto state = BaseTextInputShadowNode::updateStateIfNeeded(
layoutContext, getConcreteProps(), getStateData());
state.has_value()) {
setStateData(std::move(state.value()));
}
ConcreteViewShadowNode::layout(layoutContext);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#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>
Expand All @@ -28,52 +29,40 @@ class AndroidTextInputShadowNode final
AndroidTextInputProps,
AndroidTextInputEventEmitter,
TextInputState,
/* usesMapBufferForStateData */ true> {
/* usesMapBufferForStateData */ true>,
public BaseTextInputShadowNode {
public:
~AndroidTextInputShadowNode() noexcept override = default;

using ConcreteViewShadowNode::ConcreteViewShadowNode;

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

using ConcreteViewShadowNode::ConcreteViewShadowNode;

/*
* Returns a `AttributedString` which represents text content of the node.
*/
AttributedString getAttributedString() const;
AttributedString getPlaceholderAttributedString() const;
bool hasMeaningfulState() const override {
return getState() &&
getState()->getRevision() != State::initialRevisionValue;
}

/*
* Associates a shared TextLayoutManager with the node.
* `TextInputShadowNode` uses the manager to measure text content
* and construct `TextInputState` objects.
*/
void setTextLayoutManager(SharedTextLayoutManager textLayoutManager);
const ShadowNode& getShadowNode() const override {
return *this;
}

#pragma mark - LayoutableShadowNode
void ensureUnsealed() const override {
Sealable::ensureUnsealed();
}

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

SharedTextLayoutManager textLayoutManager_;
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@

#include "TextInputShadowNode.h"

#include <react/renderer/core/LayoutContext.h>

namespace facebook::react {

extern const char TextInputComponentName[] = "TextInput";
Expand Down

0 comments on commit 6fdc784

Please sign in to comment.