diff --git a/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp b/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp index 72f5ddff291..f637f6f45fd 100644 --- a/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp +++ b/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.cpp @@ -7,15 +7,23 @@ #include "ParagraphShadowNode.h" +#include + #include +#include +#include +#include + #include "ParagraphState.h" namespace facebook { namespace react { +using Content = ParagraphShadowNode::Content; + char const ParagraphComponentName[] = "Paragraph"; -ParagraphShadowNode::Content const &ParagraphShadowNode::getContent() const { +Content const &ParagraphShadowNode::getContent() const { if (content_.has_value()) { return content_.value(); } @@ -35,6 +43,48 @@ ParagraphShadowNode::Content const &ParagraphShadowNode::getContent() const { return content_.value(); } +Content ParagraphShadowNode::getContentWithMeasuredAttachments( + LayoutContext const &layoutContext, + LayoutConstraints const &layoutConstraints) const { + auto content = getContent(); + + if (content.attachments.empty()) { + // Base case: No attachments, nothing to do. + return content; + } + + auto localLayoutConstraints = layoutConstraints; + // Having enforced minimum size for text fragments doesn't make much sense. + localLayoutConstraints.minimumSize = Size{0, 0}; + + auto &fragments = content.attributedString.getFragments(); + + for (auto const &attachment : content.attachments) { + auto laytableShadowNode = + traitCast(attachment.shadowNode); + + if (!laytableShadowNode) { + continue; + } + + auto size = + laytableShadowNode->measure(layoutContext, localLayoutConstraints); + + // Rounding to *next* value on the pixel grid. + size.width += 0.01; + size.height += 0.01; + size = roundToPixel<&ceil>(size, layoutContext.pointScaleFactor); + + auto fragmentLayoutMetrics = LayoutMetrics{}; + fragmentLayoutMetrics.pointScaleFactor = layoutContext.pointScaleFactor; + fragmentLayoutMetrics.frame.size = size; + fragments[attachment.fragmentIndex].parentShadowView.layoutMetrics = + fragmentLayoutMetrics; + } + + return content; +} + void ParagraphShadowNode::setTextLayoutManager( SharedTextLayoutManager textLayoutManager) { ensureUnsealed(); @@ -64,7 +114,8 @@ void ParagraphShadowNode::updateStateIfNeeded(Content const &content) { #pragma mark - LayoutableShadowNode Size ParagraphShadowNode::measure(LayoutConstraints layoutConstraints) const { - auto content = getContent(); + auto content = + getContentWithMeasuredAttachments(LayoutContext{}, layoutConstraints); if (content.attributedString.isEmpty()) { return layoutConstraints.clamp({0, 0}); @@ -79,8 +130,88 @@ Size ParagraphShadowNode::measure(LayoutConstraints layoutConstraints) const { } void ParagraphShadowNode::layout(LayoutContext layoutContext) { - updateStateIfNeeded(getContent()); - ConcreteViewShadowNode::layout(layoutContext); + ensureUnsealed(); + + auto layoutMetrics = getLayoutMetrics(); + auto availableSize = layoutMetrics.getContentFrame().size; + + auto layoutConstraints = LayoutConstraints{ + availableSize, availableSize, layoutMetrics.layoutDirection}; + auto content = + getContentWithMeasuredAttachments(layoutContext, layoutConstraints); + + updateStateIfNeeded(content); + + if (content.attachments.empty()) { + // No attachments, nothing to layout. + return; + } + + auto measurement = textLayoutManager_->measure( + AttributedStringBox{content.attributedString}, + content.paragraphAttributes, + layoutConstraints); + + // Iterating on attachments, we clone shadow nodes and moving + // `paragraphShadowNode` that represents clones of `this` object. + auto paragraphShadowNode = static_cast(this); + // `paragraphOwningShadowNode` is owning pointer to`paragraphShadowNode` + // (besides the initial case when `paragraphShadowNode == this`), we need this + // only to keep it in memory for a while. + auto paragraphOwningShadowNode = ShadowNode::Unshared{}; + + assert(content.attachments.size() == measurement.attachments.size()); + + for (auto i = 0; i < content.attachments.size(); i++) { + auto &attachment = content.attachments.at(i); + + if (!traitCast(attachment.shadowNode)) { + // Not a layoutable `ShadowNode`, no need to lay it out. + continue; + } + + auto clonedShadowNode = ShadowNode::Unshared{}; + + paragraphOwningShadowNode = paragraphShadowNode->cloneTree( + attachment.shadowNode->getFamily(), + [&](ShadowNode const &oldShadowNode) { + clonedShadowNode = oldShadowNode.clone({}); + return clonedShadowNode; + }); + paragraphShadowNode = + static_cast(paragraphOwningShadowNode.get()); + + auto &layoutableShadowNode = const_cast( + traitCast(*clonedShadowNode)); + + auto attachmentFrame = measurement.attachments[i].frame; + auto attachmentSize = roundToPixel<&ceil>( + attachmentFrame.size, layoutMetrics.pointScaleFactor); + auto attachmentOrigin = roundToPixel<&round>( + attachmentFrame.origin, layoutMetrics.pointScaleFactor); + auto attachmentLayoutContext = layoutContext; + attachmentLayoutContext.absolutePosition += attachmentOrigin; + auto attachmentLayoutConstrains = LayoutConstraints{ + attachmentSize, attachmentSize, layoutConstraints.layoutDirection}; + + // Laying out the `ShadowNode` and the subtree starting from it. + layoutableShadowNode.layoutTree( + attachmentLayoutContext, attachmentLayoutConstrains); + + // Altering the origin of the `ShadowNode` (which is defined by text layout, + // not by internal styles and state). + auto attachmentLayoutMetrics = layoutableShadowNode.getLayoutMetrics(); + attachmentLayoutMetrics.frame.origin = attachmentOrigin; + layoutableShadowNode.setLayoutMetrics(attachmentLayoutMetrics); + } + + // If we ended up cloning something, we need to update the list of children to + // reflect the changes that we made. + if (paragraphShadowNode != this) { + this->children_ = + static_cast(paragraphShadowNode) + ->children_; + } } } // namespace react diff --git a/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.h b/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.h index b5874d01391..87255915826 100644 --- a/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.h +++ b/ReactCommon/fabric/components/text/paragraph/ParagraphShadowNode.h @@ -57,7 +57,6 @@ class ParagraphShadowNode : public ConcreteViewShadowNode< void layout(LayoutContext layoutContext) override; Size measure(LayoutConstraints layoutConstraints) const override; - private: /* * Internal representation of the nested content of the node in a format * suitable for future processing. @@ -69,11 +68,19 @@ class ParagraphShadowNode : public ConcreteViewShadowNode< Attachments attachments; }; + private: /* * Builds (if needed) and returns a reference to a `Content` object. */ Content const &getContent() const; + /* + * Builds and returns a `Content` object with given `layoutConstraints`. + */ + Content getContentWithMeasuredAttachments( + LayoutContext const &layoutContext, + LayoutConstraints const &layoutConstraints) const; + /* * Creates a `State` object (with `AttributedText` and * `TextLayoutManager`) if needed.