/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "ParagraphShadowNode.h" #include #include #include #include #include #include #include #include #include "ParagraphState.h" namespace facebook::react { using Content = ParagraphShadowNode::Content; char const ParagraphComponentName[] = "Paragraph"; Content const &ParagraphShadowNode::getContent( LayoutContext const &layoutContext) const { if (content_.has_value()) { return content_.value(); } ensureUnsealed(); auto textAttributes = TextAttributes::defaultTextAttributes(); textAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; textAttributes.apply(getConcreteProps().textAttributes); textAttributes.layoutDirection = YGNodeLayoutGetDirection(&yogaNode_) == YGDirectionRTL ? LayoutDirection::RightToLeft : LayoutDirection::LeftToRight; auto attributedString = AttributedString{}; auto attachments = Attachments{}; buildAttributedString(textAttributes, *this, attributedString, attachments); content_ = Content{ attributedString, getConcreteProps().paragraphAttributes, attachments}; return content_.value(); } Content ParagraphShadowNode::getContentWithMeasuredAttachments( LayoutContext const &layoutContext, LayoutConstraints const &layoutConstraints) const { auto content = getContent(layoutContext); 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 == nullptr) { continue; } auto size = laytableShadowNode->measure(layoutContext, localLayoutConstraints); // Rounding to *next* value on the pixel grid. size.width += 0.01f; size.height += 0.01f; 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( std::shared_ptr textLayoutManager) { ensureUnsealed(); getStateData().paragraphLayoutManager.setTextLayoutManager( std::move(textLayoutManager)); } void ParagraphShadowNode::updateStateIfNeeded(Content const &content) { ensureUnsealed(); auto &state = getStateData(); react_native_assert(state.paragraphLayoutManager.getTextLayoutManager()); if (state.attributedString == content.attributedString) { return; } setStateData(ParagraphState{ content.attributedString, content.paragraphAttributes, state.paragraphLayoutManager}); } #pragma mark - LayoutableShadowNode Size ParagraphShadowNode::measureContent( LayoutContext const &layoutContext, LayoutConstraints const &layoutConstraints) const { auto content = getContentWithMeasuredAttachments(layoutContext, layoutConstraints); auto attributedString = content.attributedString; if (attributedString.isEmpty()) { // Note: `zero-width space` is insufficient in some cases (e.g. when we need // to measure the "height" of the font). // TODO T67606511: We will redefine the measurement of empty strings as part // of T67606511 auto string = BaseTextShadowNode::getEmptyPlaceholder(); auto textAttributes = TextAttributes::defaultTextAttributes(); textAttributes.fontSizeMultiplier = layoutContext.fontSizeMultiplier; textAttributes.apply(getConcreteProps().textAttributes); attributedString.appendFragment({string, textAttributes, {}}); } return getStateData() .paragraphLayoutManager .measure(attributedString, content.paragraphAttributes, layoutConstraints) .size; } void ParagraphShadowNode::layout(LayoutContext layoutContext) { ensureUnsealed(); auto layoutMetrics = getLayoutMetrics(); auto availableSize = layoutMetrics.getContentFrame().size; auto layoutConstraints = LayoutConstraints{ availableSize, availableSize, layoutMetrics.layoutDirection}; auto content = getContentWithMeasuredAttachments(layoutContext, layoutConstraints); updateStateIfNeeded(content); auto measurement = getStateData().paragraphLayoutManager.measure( content.attributedString, content.paragraphAttributes, layoutConstraints); if (getConcreteProps().onTextLayout) { auto linesMeasurements = getStateData().paragraphLayoutManager.measureLines( content.attributedString, content.paragraphAttributes, measurement.size); getConcreteEventEmitter().onTextLayout(linesMeasurements); } if (content.attachments.empty()) { // No attachments to layout. return; } // 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{}; react_native_assert( content.attachments.size() == measurement.attachments.size()); for (size_t i = 0; i < content.attachments.size(); i++) { auto &attachment = content.attachments.at(i); if (traitCast(attachment.shadowNode) == nullptr) { // 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 = 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; 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 facebook::react