Skip to content

Commit

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

Summary:
Pull Request resolved: #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 10, 2024
1 parent 7d8139e commit d073f1d
Show file tree
Hide file tree
Showing 3 changed files with 7 additions and 234 deletions.
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 @@ -206,8 +208,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(
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,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,57 +21,18 @@ extern const char AndroidTextInputComponentName[];
* `ShadowNode` for <AndroidTextInput> component.
*/
class AndroidTextInputShadowNode final
: public ConcreteViewShadowNode<
: public BaseTextInputShadowNode<
AndroidTextInputComponentName,
AndroidTextInputProps,
AndroidTextInputEventEmitter,
TextInputState,
/* usesMapBufferForStateData */ true> {
public:
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;

/*
* Associates a shared TextLayoutManager with the node.
* `TextInputShadowNode` uses the manager to measure text content
* and construct `TextInputState` objects.
*/
void setTextLayoutManager(SharedTextLayoutManager textLayoutManager);

#pragma mark - LayoutableShadowNode
using BaseTextInputShadowNode::BaseTextInputShadowNode;

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

0 comments on commit d073f1d

Please sign in to comment.