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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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