| // Copyright 2016 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h" |
| |
| #include <memory> |
| #include <numeric> |
| |
| #include "base/containers/adapters.h" |
| #include "base/debug/dump_without_crashing.h" |
| #include "base/ranges/algorithm.h" |
| #include "base/trace_event/trace_event.h" |
| #include "third_party/blink/renderer/core/frame/web_feature.h" |
| #include "third_party/blink/renderer/core/layout/layout_block_flow.h" |
| #include "third_party/blink/renderer/core/layout/layout_counter.h" |
| #include "third_party/blink/renderer/core/layout/layout_inline.h" |
| #include "third_party/blink/renderer/core/layout/layout_object.h" |
| #include "third_party/blink/renderer/core/layout/layout_object_inlines.h" |
| #include "third_party/blink/renderer/core/layout/layout_text.h" |
| #include "third_party/blink/renderer/core/layout/list_marker.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/layout_ng_text_combine.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_bidi_paragraph.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_initial_letter_utils.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_line_info.h" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h" |
| #include "third_party/blink/renderer/core/layout/ng/legacy_layout_tree_walking.h" |
| #include "third_party/blink/renderer/core/layout/ng/list/layout_ng_inline_list_item.h" |
| #include "third_party/blink/renderer/core/layout/ng/list/layout_ng_list_item.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_block_break_token.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_layout_result.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_length_utils.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_positioned_float.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_space_utils.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_unpositioned_float.h" |
| #include "third_party/blink/renderer/core/layout/ng/svg/ng_svg_text_layout_attributes_builder.h" |
| #include "third_party/blink/renderer/core/layout/ng/svg/svg_inline_node_data.h" |
| #include "third_party/blink/renderer/core/style/computed_style.h" |
| #include "third_party/blink/renderer/core/style/computed_style_base_constants.h" |
| #include "third_party/blink/renderer/platform/fonts/font_performance.h" |
| #include "third_party/blink/renderer/platform/fonts/shaping/harfbuzz_shaper.h" |
| #include "third_party/blink/renderer/platform/fonts/shaping/ng_shape_cache.h" |
| #include "third_party/blink/renderer/platform/fonts/shaping/run_segmenter.h" |
| #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h" |
| #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_view.h" |
| #include "third_party/blink/renderer/platform/heap/collection_support/clear_collection_scope.h" |
| #include "third_party/blink/renderer/platform/runtime_enabled_features.h" |
| #include "third_party/blink/renderer/platform/wtf/text/character_names.h" |
| #include "third_party/blink/renderer/platform/wtf/text/string_buffer.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // Returns sum of |ShapeResult::Width()| in |data.items|. Note: All items |
| // should be text item other type of items are not allowed. |
| float CalculateWidthForTextCombine(const NGInlineItemsData& data) { |
| return std::accumulate( |
| data.items.begin(), data.items.end(), 0.0f, |
| [](float sum, const NGInlineItem& item) { |
| DCHECK(item.Type() == NGInlineItem::kText || |
| item.Type() == NGInlineItem::kBidiControl || |
| item.Type() == NGInlineItem::kControl) |
| << item.Type(); |
| if (auto* const shape_result = item.TextShapeResult()) |
| return shape_result->Width() + sum; |
| return 0.0f; |
| }); |
| } |
| |
| // Estimate the number of NGInlineItem to minimize the vector expansions. |
| unsigned EstimateInlineItemsCount(const LayoutBlockFlow& block) { |
| unsigned count = 0; |
| for (LayoutObject* child = block.FirstChild(); child; |
| child = child->NextSibling()) { |
| ++count; |
| } |
| return count * 4; |
| } |
| |
| // Estimate the number of units and ranges in NGOffsetMapping to minimize vector |
| // and hash map expansions. |
| unsigned EstimateOffsetMappingItemsCount(const LayoutBlockFlow& block) { |
| // Cancels out the factor 4 in EstimateInlineItemsCount() to get the number of |
| // LayoutObjects. |
| // TODO(layout-dev): Unify the two functions and make them less hacky. |
| return EstimateInlineItemsCount(block) / 4; |
| } |
| |
| // Wrapper over ShapeText that re-uses existing shape results for items that |
| // haven't changed. |
| class ReusingTextShaper final { |
| STACK_ALLOCATED(); |
| |
| public: |
| ReusingTextShaper(NGInlineItemsData* data, |
| const HeapVector<NGInlineItem>* reusable_items, |
| const bool allow_shape_cache) |
| : data_(*data), |
| reusable_items_(reusable_items), |
| shaper_(data->text_content), |
| allow_shape_cache_(allow_shape_cache) {} |
| |
| scoped_refptr<const ShapeResult> Shape(const NGInlineItem& start_item, |
| const Font& font, |
| unsigned end_offset) { |
| ShapeCacheEntry* entry = nullptr; |
| if (allow_shape_cache_) { |
| DCHECK(RuntimeEnabledFeatures::LayoutNGShapeCacheEnabled()); |
| NGShapeCache* cache = font.GetNGShapeCache(); |
| const TextDirection direction = start_item.Direction(); |
| entry = cache->Add(shaper_.GetText(), direction); |
| if (entry && *entry) { |
| return *entry; |
| } |
| } |
| scoped_refptr<const ShapeResult> result = |
| ShapeWithoutCache(start_item, font, end_offset); |
| if (entry) { |
| *entry = result; |
| } |
| return result; |
| } |
| |
| scoped_refptr<ShapeResult> ShapeWithoutCache(const NGInlineItem& start_item, |
| const Font& font, |
| unsigned end_offset) { |
| const unsigned start_offset = start_item.StartOffset(); |
| DCHECK_LT(start_offset, end_offset); |
| |
| if (!reusable_items_) |
| return Reshape(start_item, font, start_offset, end_offset); |
| |
| // TODO(yosin): We should support segment text |
| if (data_.segments) |
| return Reshape(start_item, font, start_offset, end_offset); |
| |
| const Vector<const ShapeResult*> reusable_shape_results = |
| CollectReusableShapeResults(start_offset, end_offset, |
| font.PrimaryFont(), start_item.Direction()); |
| if (reusable_shape_results.empty()) |
| return Reshape(start_item, font, start_offset, end_offset); |
| |
| const scoped_refptr<ShapeResult> shape_result = |
| ShapeResult::CreateEmpty(*reusable_shape_results.front()); |
| unsigned offset = start_offset; |
| for (const ShapeResult* reusable_shape_result : reusable_shape_results) { |
| // In case of pre-wrap having break opportunity after leading space, |
| // |offset| can be greater than |reusable_shape_result->StartIndex()|. |
| // e.g. <div style="white-space:pre"> abc</div>, deleteChar(0, 1) |
| // See xternal/wpt/editing/run/delete.html?993-993 |
| if (offset < reusable_shape_result->StartIndex()) { |
| AppendShapeResult(*Reshape(start_item, font, offset, |
| reusable_shape_result->StartIndex()), |
| shape_result.get()); |
| offset = shape_result->EndIndex(); |
| } |
| DCHECK_LT(offset, reusable_shape_result->EndIndex()); |
| DCHECK(shape_result->NumCharacters() == 0 || |
| shape_result->EndIndex() == offset); |
| reusable_shape_result->CopyRange( |
| offset, std::min(reusable_shape_result->EndIndex(), end_offset), |
| shape_result.get()); |
| offset = shape_result->EndIndex(); |
| if (offset == end_offset) |
| return shape_result; |
| } |
| DCHECK_LT(offset, end_offset); |
| AppendShapeResult(*Reshape(start_item, font, offset, end_offset), |
| shape_result.get()); |
| return shape_result; |
| } |
| |
| private: |
| void AppendShapeResult(const ShapeResult& shape_result, ShapeResult* target) { |
| DCHECK(target->NumCharacters() == 0 || |
| target->EndIndex() == shape_result.StartIndex()); |
| shape_result.CopyRange(shape_result.StartIndex(), shape_result.EndIndex(), |
| target); |
| } |
| |
| Vector<const ShapeResult*> CollectReusableShapeResults( |
| unsigned start_offset, |
| unsigned end_offset, |
| const SimpleFontData* primary_font, |
| TextDirection direction) { |
| DCHECK_LT(start_offset, end_offset); |
| Vector<const ShapeResult*> shape_results; |
| if (!reusable_items_) |
| return shape_results; |
| for (const NGInlineItem *item = std::lower_bound( |
| reusable_items_->begin(), reusable_items_->end(), start_offset, |
| [](const NGInlineItem&item, unsigned offset) { |
| return item.EndOffset() <= offset; |
| }); |
| item != reusable_items_->end(); ++item) { |
| if (end_offset <= item->StartOffset()) |
| break; |
| if (item->EndOffset() < start_offset) |
| continue; |
| const ShapeResult* const shape_result = item->TextShapeResult(); |
| if (!shape_result || item->Direction() != direction) |
| continue; |
| if (shape_result->PrimaryFont() != primary_font) |
| continue; |
| if (shape_result->IsAppliedSpacing()) |
| continue; |
| shape_results.push_back(shape_result); |
| } |
| return shape_results; |
| } |
| |
| scoped_refptr<ShapeResult> Reshape(const NGInlineItem& start_item, |
| const Font& font, |
| unsigned start_offset, |
| unsigned end_offset) { |
| DCHECK_LT(start_offset, end_offset); |
| const TextDirection direction = start_item.Direction(); |
| if (data_.segments) { |
| return data_.segments->ShapeText( |
| &shaper_, &font, direction, start_offset, end_offset, |
| base::checked_cast<unsigned>(&start_item - data_.items.begin())); |
| } |
| RunSegmenter::RunSegmenterRange range = |
| start_item.CreateRunSegmenterRange(); |
| range.end = end_offset; |
| return shaper_.Shape(&font, direction, start_offset, end_offset, range); |
| } |
| |
| NGInlineItemsData& data_; |
| const HeapVector<NGInlineItem>* const reusable_items_; |
| HarfBuzzShaper shaper_; |
| const bool allow_shape_cache_; |
| }; |
| |
| // The function is templated to indicate the purpose of collected inlines: |
| // - With EmptyOffsetMappingBuilder: updating layout; |
| // - With NGOffsetMappingBuilder: building offset mapping on clean layout. |
| // |
| // This allows code sharing between the two purposes with slightly different |
| // behaviors. For example, we clear a LayoutObject's need layout flags when |
| // updating layout, but don't do that when building offset mapping. |
| // |
| // There are also performance considerations, since template saves the overhead |
| // for condition checking and branching. |
| template <typename ItemsBuilder> |
| void CollectInlinesInternal(ItemsBuilder* builder, |
| const NGInlineNodeData* previous_data) { |
| LayoutBlockFlow* const block = builder->GetLayoutBlockFlow(); |
| builder->EnterBlock(block->Style()); |
| LayoutObject* node = GetLayoutObjectForFirstChildNode(block); |
| |
| const LayoutObject* symbol = |
| LayoutNGListItem::FindSymbolMarkerLayoutText(block); |
| const LayoutObject* inline_list_item_marker = nullptr; |
| while (node) { |
| if (auto* counter = DynamicTo<LayoutCounter>(node)) { |
| // According to |
| // https://w3c.github.io/csswg-drafts/css-counter-styles/#simple-symbolic, |
| // disclosure-* should have special rendering paths. |
| if (counter->IsDirectionalSymbolMarker()) { |
| const String& text = counter->GetText(); |
| // We assume the text representation length for a predefined symbol |
| // marker is always 1. |
| if (text.length() <= 1) { |
| builder->AppendText(counter, previous_data); |
| builder->SetIsSymbolMarker(); |
| } else { |
| // The text must be in the following form: |
| // Symbol, separator, symbol, separator, symbol, ... |
| builder->AppendText(text.Substring(0, 1), counter); |
| builder->SetIsSymbolMarker(); |
| const AtomicString& separator = counter->Separator(); |
| for (wtf_size_t i = 1; i < text.length();) { |
| if (separator.length() > 0) { |
| DCHECK_EQ(separator, text.Substring(i, separator.length())); |
| builder->AppendText(separator, counter); |
| i += separator.length(); |
| DCHECK_LT(i, text.length()); |
| } |
| builder->AppendText(text.Substring(i, 1), counter); |
| builder->SetIsSymbolMarker(); |
| ++i; |
| } |
| } |
| } else { |
| builder->AppendText(counter, previous_data); |
| } |
| builder->ClearNeedsLayout(counter); |
| } else if (auto* layout_text = DynamicTo<LayoutText>(node)) { |
| builder->AppendText(layout_text, previous_data); |
| |
| if (symbol == layout_text || inline_list_item_marker == layout_text) { |
| builder->SetIsSymbolMarker(); |
| } |
| |
| builder->ClearNeedsLayout(layout_text); |
| } else if (node->IsFloating()) { |
| builder->AppendFloating(node); |
| if (builder->ShouldAbort()) |
| return; |
| |
| builder->ClearInlineFragment(node); |
| } else if (node->IsOutOfFlowPositioned()) { |
| builder->AppendOutOfFlowPositioned(node); |
| if (builder->ShouldAbort()) |
| return; |
| |
| builder->ClearInlineFragment(node); |
| } else if (node->IsAtomicInlineLevel()) { |
| if (node->IsBoxListMarkerIncludingNG()) { |
| // LayoutNGListItem produces the 'outside' list marker as an inline |
| // block. This is an out-of-flow item whose position is computed |
| // automatically. |
| builder->AppendOpaque(NGInlineItem::kListMarker, node); |
| } else if (UNLIKELY(node->IsInitialLetterBox())) { |
| builder->AppendOpaque(NGInlineItem::kInitialLetterBox, |
| kObjectReplacementCharacter, node); |
| builder->SetHasInititialLetterBox(); |
| } else { |
| // For atomic inlines add a unicode "object replacement character" to |
| // signal the presence of a non-text object to the unicode bidi |
| // algorithm. |
| builder->AppendAtomicInline(node); |
| } |
| builder->ClearInlineFragment(node); |
| } else if (auto* layout_inline = DynamicTo<LayoutInline>(node)) { |
| if (auto* inline_list_item = DynamicTo<LayoutNGInlineListItem>(node)) { |
| inline_list_item->UpdateMarkerTextIfNeeded(); |
| inline_list_item_marker = |
| LayoutNGListItem::FindSymbolMarkerLayoutText(inline_list_item); |
| } |
| builder->UpdateShouldCreateBoxFragment(layout_inline); |
| |
| builder->EnterInline(layout_inline); |
| |
| // Traverse to children if they exist. |
| if (LayoutObject* child = layout_inline->FirstChild()) { |
| node = child; |
| continue; |
| } |
| |
| // An empty inline node. |
| builder->ExitInline(layout_inline); |
| builder->ClearNeedsLayout(layout_inline); |
| } else { |
| DCHECK(!node->IsInline()); |
| builder->AppendBlockInInline(node); |
| builder->ClearInlineFragment(node); |
| } |
| |
| // Find the next sibling, or parent, until we reach |block|. |
| while (true) { |
| if (LayoutObject* next = node->NextSibling()) { |
| node = next; |
| break; |
| } |
| node = GetLayoutObjectForParentNode(node); |
| if (node == block || !node) { |
| // Set |node| to |nullptr| to break out of the outer loop. |
| node = nullptr; |
| break; |
| } |
| DCHECK(node->IsInline()); |
| builder->ExitInline(node); |
| builder->ClearNeedsLayout(node); |
| } |
| } |
| builder->ExitBlock(); |
| } |
| |
| // Returns whether this text should break shaping. Even within a box, text runs |
| // that have different shaping properties need to break shaping. |
| inline bool ShouldBreakShapingBeforeText(const NGInlineItem& item, |
| const NGInlineItem& start_item, |
| const ComputedStyle& start_style, |
| const Font& start_font, |
| TextDirection start_direction) { |
| DCHECK_EQ(item.Type(), NGInlineItem::kText); |
| DCHECK(item.Style()); |
| const ComputedStyle& style = *item.Style(); |
| if (&style != &start_style) { |
| const Font& font = style.GetFont(); |
| if (&font != &start_font && font != start_font) |
| return true; |
| } |
| |
| // The resolved direction and run segment properties must match to shape |
| // across for HarfBuzzShaper. |
| return item.Direction() != start_direction || |
| !item.EqualsRunSegment(start_item); |
| } |
| |
| // Returns whether the start of this box should break shaping. |
| inline bool ShouldBreakShapingBeforeBox(const NGInlineItem& item) { |
| DCHECK_EQ(item.Type(), NGInlineItem::kOpenTag); |
| DCHECK(item.Style()); |
| const ComputedStyle& style = *item.Style(); |
| |
| // These properties values must break shaping. |
| // https://drafts.csswg.org/css-text-3/#boundary-shaping |
| if ((style.MayHavePadding() && !style.PaddingStart().IsZero()) || |
| (style.MayHaveMargin() && !style.MarginStart().IsZero()) || |
| style.BorderStartWidth() || |
| style.VerticalAlign() != EVerticalAlign::kBaseline) |
| return true; |
| |
| return false; |
| } |
| |
| // Returns whether the end of this box should break shaping. |
| inline bool ShouldBreakShapingAfterBox(const NGInlineItem& item) { |
| DCHECK_EQ(item.Type(), NGInlineItem::kCloseTag); |
| DCHECK(item.Style()); |
| const ComputedStyle& style = *item.Style(); |
| |
| // These properties values must break shaping. |
| // https://drafts.csswg.org/css-text-3/#boundary-shaping |
| if ((style.MayHavePadding() && !style.PaddingEnd().IsZero()) || |
| (style.MayHaveMargin() && !style.MarginEnd().IsZero()) || |
| style.BorderEndWidth() || |
| style.VerticalAlign() != EVerticalAlign::kBaseline) |
| return true; |
| |
| return false; |
| } |
| |
| inline bool NeedsShaping(const NGInlineItem& item) { |
| if (item.Type() != NGInlineItem::kText) |
| return false; |
| // Text item with length==0 exists to maintain LayoutObject states such as |
| // ClearNeedsLayout, but not needed to shape. |
| if (!item.Length()) { |
| return false; |
| } |
| if (item.IsUnsafeToReuseShapeResult()) { |
| return true; |
| } |
| const ShapeResult* shape_result = item.TextShapeResult(); |
| if (!shape_result) |
| return true; |
| // |StartOffset| is usually safe-to-break, but it is not when we shape across |
| // elements and split the |ShapeResult|. Such |ShapeResult| is not safe to |
| // reuse. |
| DCHECK_EQ(item.StartOffset(), shape_result->StartIndex()); |
| if (!shape_result->IsStartSafeToBreak()) |
| return true; |
| return false; |
| } |
| |
| // Determine if reshape is needed for ::first-line style. |
| bool FirstLineNeedsReshape(const ComputedStyle& first_line_style, |
| const ComputedStyle& base_style) { |
| const Font& base_font = base_style.GetFont(); |
| const Font& first_line_font = first_line_style.GetFont(); |
| return &base_font != &first_line_font && base_font != first_line_font; |
| } |
| |
| // Make a string to the specified length, either by truncating if longer, or |
| // appending space characters if shorter. |
| void TruncateOrPadText(String* text, unsigned length) { |
| if (text->length() > length) { |
| *text = text->Substring(0, length); |
| } else if (text->length() < length) { |
| StringBuilder builder; |
| builder.ReserveCapacity(length); |
| builder.Append(*text); |
| while (builder.length() < length) |
| builder.Append(kSpaceCharacter); |
| *text = builder.ToString(); |
| } |
| } |
| |
| } // namespace |
| |
| NGInlineNode::NGInlineNode(LayoutBlockFlow* block) |
| : NGLayoutInputNode(block, kInline) { |
| DCHECK(block); |
| DCHECK(block->IsLayoutNGObject()); |
| if (!block->HasNGInlineNodeData()) |
| block->ResetNGInlineNodeData(); |
| } |
| |
| bool NGInlineNode::IsPrepareLayoutFinished() const { |
| const NGInlineNodeData* data = |
| To<LayoutBlockFlow>(box_.Get())->GetNGInlineNodeData(); |
| return data && !data->text_content.IsNull(); |
| } |
| |
| void NGInlineNode::PrepareLayoutIfNeeded() const { |
| NGInlineNodeData* previous_data = nullptr; |
| LayoutBlockFlow* block_flow = GetLayoutBlockFlow(); |
| if (IsPrepareLayoutFinished()) { |
| if (!block_flow->NeedsCollectInlines()) |
| return; |
| |
| // Note: For "text-combine-upright:all", we use a font calculated from |
| // text width, so we can't reuse previous data. |
| if (LIKELY(!IsTextCombine())) |
| previous_data = block_flow->TakeNGInlineNodeData(); |
| block_flow->ResetNGInlineNodeData(); |
| } |
| |
| PrepareLayout(previous_data); |
| |
| if (previous_data) { |
| // previous_data is not used from now on but exists until GC happens, so it |
| // is better to eagerly clear HeapVector to improve memory utilization. |
| previous_data->items.clear(); |
| } |
| } |
| |
| void NGInlineNode::PrepareLayout(NGInlineNodeData* previous_data) const { |
| // Scan list of siblings collecting all in-flow non-atomic inlines. A single |
| // NGInlineNode represent a collection of adjacent non-atomic inlines. |
| NGInlineNodeData* data = MutableData(); |
| DCHECK(data); |
| CollectInlines(data, previous_data); |
| SegmentText(data); |
| ShapeTextIncludingFirstLine( |
| data, previous_data ? &previous_data->text_content : nullptr, nullptr); |
| AssociateItemsWithInlines(data); |
| DCHECK_EQ(data, MutableData()); |
| |
| LayoutBlockFlow* block_flow = GetLayoutBlockFlow(); |
| block_flow->ClearNeedsCollectInlines(); |
| |
| if (UNLIKELY(IsTextCombine())) { |
| // To use |LayoutNGTextCombine::UsersScaleX()| in |NGFragmentItemsBuilder|, |
| // we adjust font here instead in |Layout()|, |
| AdjustFontForTextCombineUprightAll(); |
| } |
| |
| #if DCHECK_IS_ON() |
| // ComputeOffsetMappingIfNeeded() runs some integrity checks as part of |
| // creating offset mapping. Run the check, and discard the result. |
| DCHECK(!data->offset_mapping); |
| ComputeOffsetMappingIfNeeded(); |
| DCHECK(data->offset_mapping); |
| data->offset_mapping.Clear(); |
| #endif |
| } |
| |
| // Building |NGInlineNodeData| for |LayoutText::SetTextWithOffset()| with |
| // reusing data. |
| class NGInlineNodeDataEditor final { |
| STACK_ALLOCATED(); |
| |
| public: |
| explicit NGInlineNodeDataEditor(const LayoutText& layout_text) |
| : block_flow_(layout_text.FragmentItemsContainer()), |
| layout_text_(layout_text) { |
| DCHECK(layout_text_.HasValidInlineItems()); |
| } |
| NGInlineNodeDataEditor(const NGInlineNodeDataEditor&) = delete; |
| NGInlineNodeDataEditor& operator=(const NGInlineNodeDataEditor&) = delete; |
| |
| LayoutBlockFlow* GetLayoutBlockFlow() const { return block_flow_; } |
| |
| // Note: We can't use |Position| for |layout_text_.GetNode()| because |Text| |
| // node is already changed. |
| NGInlineNodeData* Prepare(unsigned offset, unsigned length) { |
| if (!block_flow_ || block_flow_->NeedsCollectInlines() || |
| block_flow_->NeedsLayout() || |
| block_flow_->GetDocument().NeedsLayoutTreeUpdate() || |
| !block_flow_->GetNGInlineNodeData() || |
| block_flow_->GetNGInlineNodeData()->text_content.IsNull() || |
| block_flow_->GetNGInlineNodeData()->items.empty()) |
| return nullptr; |
| |
| // For "text-combine-upright:all", we choose font to fit layout result in |
| // 1em, so font can be different than original font. |
| if (UNLIKELY(IsA<LayoutNGTextCombine>(block_flow_))) |
| return nullptr; |
| |
| // Because of current text content has secured text, e.g. whole text is |
| // "***", all characters including collapsed white spaces are marker, and |
| // new text is original text, we can't reuse shape result. |
| if (layout_text_.StyleRef().TextSecurity() != ETextSecurity::kNone) |
| return nullptr; |
| |
| // It is hard to figure differences of bidi control codes before/after |
| // editing. See http://crbug.com/1039143 |
| if (layout_text_.HasBidiControlInlineItems()) |
| return nullptr; |
| |
| // Note: We should compute offset mapping before calling |
| // |LayoutBlockFlow::TakeNGInlineNodeData()| |
| const NGOffsetMapping* const offset_mapping = |
| NGInlineNode::GetOffsetMapping(block_flow_); |
| DCHECK(offset_mapping); |
| if (data_) { |
| // data_ is not used from now on but exists until GC happens, so it is |
| // better to eagerly clear HeapVector to improve memory utilization. |
| data_->items.clear(); |
| } |
| data_ = block_flow_->TakeNGInlineNodeData(); |
| return data_; |
| } |
| |
| void Run() { |
| const NGInlineNodeData& new_data = *block_flow_->GetNGInlineNodeData(); |
| const unsigned old_length = data_->text_content.length(); |
| const unsigned new_length = new_data.text_content.length(); |
| const unsigned start_offset = Mismatch(*data_, new_data); |
| // * "ab cd ef" => delete "cd" => "ab ef" |
| // We should not reuse " " before "ef" |
| // * "a bc" => delete "bc" => "a" |
| // There are no spaces after "a". |
| const unsigned matched_length = MismatchFromEnd( |
| *data_, new_data, |
| std::min(old_length - start_offset, new_length - start_offset)); |
| DCHECK_LE(start_offset, old_length - matched_length); |
| DCHECK_LE(start_offset, new_length - matched_length); |
| const unsigned end_offset = old_length - matched_length; |
| DCHECK_LE(start_offset, end_offset); |
| HeapVector<NGInlineItem> items; |
| ClearCollectionScope<HeapVector<NGInlineItem>> clear_scope(&items); |
| |
| // +3 for before and after replaced text. |
| items.ReserveInitialCapacity(data_->items.size() + 3); |
| |
| // Copy items before replaced range |
| auto const* end = data_->items.end(); |
| auto* it = data_->items.begin(); |
| while (it != end && it->end_offset_ < start_offset) { |
| DCHECK(it != data_->items.end()); |
| items.push_back(*it); |
| ++it; |
| } |
| |
| for (;;) { |
| if (it == end) |
| break; |
| |
| // Copy part of item before replaced range. |
| if (it->start_offset_ < start_offset) { |
| const NGInlineItem& new_item = CopyItemBefore(*it, start_offset); |
| items.push_back(new_item); |
| if (new_item.EndOffset() < start_offset) { |
| items.push_back( |
| NGInlineItem(*it, new_item.EndOffset(), start_offset, nullptr)); |
| } |
| } |
| |
| // Skip items in replaced range. |
| while (it != end && it->end_offset_ < end_offset) |
| ++it; |
| |
| if (it == end) |
| break; |
| |
| // Inserted text |
| const int diff = new_length - old_length; |
| const unsigned inserted_end = AdjustOffset(end_offset, diff); |
| if (start_offset < inserted_end) |
| items.push_back(NGInlineItem(*it, start_offset, inserted_end, nullptr)); |
| |
| // Copy part of item after replaced range. |
| if (end_offset < it->end_offset_) { |
| const NGInlineItem& new_item = CopyItemAfter(*it, end_offset); |
| if (end_offset < new_item.StartOffset()) { |
| items.push_back( |
| NGInlineItem(*it, end_offset, new_item.StartOffset(), nullptr)); |
| ShiftItem(&items.back(), diff); |
| } |
| items.push_back(new_item); |
| ShiftItem(&items.back(), diff); |
| } |
| |
| // Copy items after replaced range |
| ++it; |
| while (it != end) { |
| DCHECK_LE(end_offset, it->start_offset_); |
| items.push_back(*it); |
| ShiftItem(&items.back(), diff); |
| ++it; |
| } |
| break; |
| } |
| |
| if (items.empty()) { |
| items.push_back(NGInlineItem(data_->items.front(), 0, |
| new_data.text_content.length(), nullptr)); |
| } else if (items.back().end_offset_ < new_data.text_content.length()) { |
| items.push_back(NGInlineItem(data_->items.back(), |
| items.back().end_offset_, |
| new_data.text_content.length(), nullptr)); |
| } |
| |
| VerifyItems(items); |
| // eagerly clear HeapVector to improve memory utilization. |
| data_->items.clear(); |
| data_->items = std::move(items); |
| data_->text_content = new_data.text_content; |
| } |
| |
| private: |
| static unsigned AdjustOffset(unsigned offset, int delta) { |
| if (delta > 0) |
| return offset + delta; |
| return offset - (-delta); |
| } |
| |
| static unsigned ConvertDOMOffsetToTextContent( |
| base::span<const NGOffsetMappingUnit> units, |
| unsigned offset) { |
| auto it = |
| base::ranges::find_if(units, [offset](const NGOffsetMappingUnit& unit) { |
| return unit.DOMStart() <= offset && offset <= unit.DOMEnd(); |
| }); |
| DCHECK(it != units.end()); |
| return it->ConvertDOMOffsetToTextContent(offset); |
| } |
| |
| // Returns copy of |item| after |start_offset| (inclusive). |
| NGInlineItem CopyItemAfter(const NGInlineItem& item, |
| unsigned start_offset) const { |
| DCHECK_LE(item.start_offset_, start_offset); |
| DCHECK_LT(start_offset, item.end_offset_); |
| const unsigned safe_start_offset = GetFirstSafeToReuse(item, start_offset); |
| const unsigned end_offset = item.end_offset_; |
| if (end_offset == safe_start_offset) |
| return NGInlineItem(item, start_offset, end_offset, nullptr); |
| // To handle kerning, e.g. inserting "A" before "V", and joining in Arabic, |
| // we should not reuse first glyph. |
| // See http://crbug.com/1199331 |
| DCHECK_LT(safe_start_offset, item.end_offset_); |
| return NGInlineItem( |
| item, safe_start_offset, end_offset, |
| item.shape_result_->SubRange(safe_start_offset, end_offset)); |
| } |
| |
| // Returns copy of |item| before |end_offset| (exclusive). |
| NGInlineItem CopyItemBefore(const NGInlineItem& item, |
| unsigned end_offset) const { |
| DCHECK_LT(item.start_offset_, end_offset); |
| DCHECK_LE(end_offset, item.end_offset_); |
| const unsigned safe_end_offset = GetLastSafeToReuse(item, end_offset); |
| const unsigned start_offset = item.start_offset_; |
| // Nothing to reuse if no characters are safe to reuse. |
| if (safe_end_offset <= start_offset) |
| return NGInlineItem(item, start_offset, end_offset, nullptr); |
| // To handle kerning, e.g. "AV", we should not reuse last glyph. |
| // See http://crbug.com/1129710 |
| DCHECK_LT(safe_end_offset, item.end_offset_); |
| return NGInlineItem( |
| item, start_offset, safe_end_offset, |
| item.shape_result_->SubRange(start_offset, safe_end_offset)); |
| } |
| |
| // See also |GetLastSafeToReuse()|. |
| unsigned GetFirstSafeToReuse(const NGInlineItem& item, |
| unsigned start_offset) const { |
| DCHECK_LE(item.start_offset_, start_offset); |
| DCHECK_LE(start_offset, item.end_offset_); |
| const unsigned end_offset = item.end_offset_; |
| // TODO(yosin): It is better to utilize OpenType |usMaxContext|. |
| // For font having "fi", |usMaxContext = 2". |
| const unsigned max_context = 2; |
| const unsigned skip = max_context - 1; |
| if (!item.shape_result_ || item.shape_result_->IsAppliedSpacing() || |
| start_offset + skip >= end_offset) |
| return end_offset; |
| item.shape_result_->EnsurePositionData(); |
| // Note: Because |CachedNextSafeToBreakOffset()| assumes |start_offset| |
| // is always safe to break offset, we try to search after |start_offset|. |
| return item.shape_result_->CachedNextSafeToBreakOffset(start_offset + skip); |
| } |
| |
| // See also |GetFirstSafeToReuse()|. |
| unsigned GetLastSafeToReuse(const NGInlineItem& item, |
| unsigned end_offset) const { |
| DCHECK_LT(item.start_offset_, end_offset); |
| DCHECK_LE(end_offset, item.end_offset_); |
| const unsigned start_offset = item.start_offset_; |
| // TODO(yosin): It is better to utilize OpenType |usMaxContext|. |
| // For font having "fi", usMaxContext = 2. |
| // For Emoji with ZWJ, usMaxContext = 10. (http://crbug.com/1213235) |
| const unsigned max_context = data_->text_content.Is8Bit() ? 2 : 10; |
| const unsigned skip = max_context - 1; |
| if (!item.shape_result_ || item.shape_result_->IsAppliedSpacing() || |
| end_offset <= start_offset + skip) |
| return start_offset; |
| item.shape_result_->EnsurePositionData(); |
| // TODO(yosin): It is better to utilize OpenType |usMaxContext|. |
| // Note: Because |CachedPreviousSafeToBreakOffset()| assumes |end_offset| |
| // is always safe to break offset, we try to search before |end_offset|. |
| return item.shape_result_->CachedPreviousSafeToBreakOffset(end_offset - |
| skip); |
| } |
| |
| template <typename Span1, typename Span2> |
| static unsigned MismatchInternal(const Span1& span1, const Span2& span2) { |
| const auto old_new = base::ranges::mismatch(span1, span2); |
| return static_cast<unsigned>(old_new.first - span1.begin()); |
| } |
| |
| static unsigned Mismatch(const NGInlineItemsData& old_data, |
| const NGInlineItemsData& new_data) { |
| const String& old_text = old_data.text_content; |
| const String& new_text = new_data.text_content; |
| if (old_text.Is8Bit()) { |
| const auto old_span8 = old_text.Span8(); |
| if (new_text.Is8Bit()) |
| return MismatchInternal(old_span8, new_text.Span8()); |
| return MismatchInternal(old_span8, new_text.Span16()); |
| } |
| const auto old_span16 = old_text.Span16(); |
| if (new_text.Is8Bit()) |
| return MismatchInternal(old_span16, new_text.Span8()); |
| return MismatchInternal(old_span16, new_text.Span16()); |
| } |
| |
| template <typename Span1, typename Span2> |
| static unsigned MismatchFromEnd(const Span1& span1, const Span2& span2) { |
| const auto old_new = |
| base::ranges::mismatch(base::Reversed(span1), base::Reversed(span2)); |
| return static_cast<unsigned>(old_new.first - span1.rbegin()); |
| } |
| |
| static unsigned MismatchFromEnd(const NGInlineItemsData& old_data, |
| const NGInlineItemsData& new_data, |
| unsigned max_length) { |
| const String& old_text = old_data.text_content; |
| const String& new_text = new_data.text_content; |
| const unsigned old_length = old_text.length(); |
| const unsigned new_length = new_text.length(); |
| DCHECK_LE(max_length, old_length); |
| DCHECK_LE(max_length, new_length); |
| const unsigned old_start = old_length - max_length; |
| const unsigned new_start = new_length - max_length; |
| if (old_text.Is8Bit()) { |
| const auto old_span8 = old_text.Span8().subspan(old_start, max_length); |
| if (new_text.Is8Bit()) { |
| return MismatchFromEnd(old_span8, |
| new_text.Span8().subspan(new_start, max_length)); |
| } |
| return MismatchFromEnd(old_span8, |
| new_text.Span16().subspan(new_start, max_length)); |
| } |
| const auto old_span16 = old_text.Span16().subspan(old_start, max_length); |
| if (new_text.Is8Bit()) { |
| return MismatchFromEnd(old_span16, |
| new_text.Span8().subspan(new_start, max_length)); |
| } |
| return MismatchFromEnd(old_span16, |
| new_text.Span16().subspan(new_start, max_length)); |
| } |
| |
| static void ShiftItem(NGInlineItem* item, int delta) { |
| if (delta == 0) |
| return; |
| item->start_offset_ = AdjustOffset(item->start_offset_, delta); |
| item->end_offset_ = AdjustOffset(item->end_offset_, delta); |
| if (!item->shape_result_) |
| return; |
| item->shape_result_ = |
| item->shape_result_->CopyAdjustedOffset(item->start_offset_); |
| } |
| |
| void VerifyItems(const HeapVector<NGInlineItem>& items) const { |
| #if DCHECK_IS_ON() |
| if (items.empty()) |
| return; |
| unsigned last_offset = items.front().start_offset_; |
| for (const NGInlineItem& item : items) { |
| DCHECK_LE(item.start_offset_, item.end_offset_); |
| DCHECK_EQ(last_offset, item.start_offset_); |
| last_offset = item.end_offset_; |
| if (!item.shape_result_) |
| continue; |
| DCHECK_LT(item.start_offset_, item.end_offset_); |
| DCHECK_EQ(item.shape_result_->StartIndex(), item.start_offset_); |
| DCHECK_EQ(item.shape_result_->EndIndex(), item.end_offset_); |
| } |
| DCHECK_EQ(last_offset, |
| block_flow_->GetNGInlineNodeData()->text_content.length()); |
| #endif |
| } |
| |
| NGInlineNodeData* data_ = nullptr; |
| LayoutBlockFlow* const block_flow_; |
| const LayoutText& layout_text_; |
| }; |
| |
| // static |
| bool NGInlineNode::SetTextWithOffset(LayoutText* layout_text, |
| String new_text_in, |
| unsigned offset, |
| unsigned length) { |
| if (!layout_text->HasValidInlineItems() || |
| !layout_text->IsInLayoutNGInlineFormattingContext()) |
| return false; |
| const String old_text = layout_text->GetText(); |
| if (offset == 0 && length == old_text.length()) { |
| // We'll run collect inline items since whole text of |layout_text| is |
| // changed. |
| return false; |
| } |
| |
| NGInlineNodeDataEditor editor(*layout_text); |
| NGInlineNodeData* const previous_data = editor.Prepare(offset, length); |
| if (!previous_data) |
| return false; |
| |
| // This function runs outside of the layout phase. Prevent purging font cache |
| // while shaping. |
| FontCachePurgePreventer font_cache_purge_preventer; |
| |
| String new_text(std::move(new_text_in)); |
| layout_text->StyleRef().ApplyTextTransform(&new_text, |
| layout_text->PreviousCharacter()); |
| layout_text->SetTextInternal(new_text); |
| |
| NGInlineNode node(editor.GetLayoutBlockFlow()); |
| NGInlineNodeData* data = node.MutableData(); |
| data->items.reserve(previous_data->items.size()); |
| NGInlineItemsBuilder builder(editor.GetLayoutBlockFlow(), &data->items); |
| // TODO(yosin): We should reuse before/after |layout_text| during collecting |
| // inline items. |
| layout_text->ClearInlineItems(); |
| CollectInlinesInternal(&builder, previous_data); |
| builder.DidFinishCollectInlines(data); |
| // Relocates |ShapeResult| in |previous_data| after |offset|+|length| |
| editor.Run(); |
| node.SegmentText(data); |
| node.ShapeTextIncludingFirstLine(data, &previous_data->text_content, |
| &previous_data->items); |
| node.AssociateItemsWithInlines(data); |
| return true; |
| } |
| |
| const NGInlineNodeData& NGInlineNode::EnsureData() const { |
| PrepareLayoutIfNeeded(); |
| return Data(); |
| } |
| |
| const NGOffsetMapping* NGInlineNode::ComputeOffsetMappingIfNeeded() const { |
| DCHECK(!GetLayoutBlockFlow()->GetDocument().NeedsLayoutTreeUpdate() || |
| GetLayoutBlockFlow()->IsLayoutNGObjectForFormattedText()); |
| |
| NGInlineNodeData* data = MutableData(); |
| if (!data->offset_mapping) { |
| DCHECK(!data->text_content.IsNull()); |
| ComputeOffsetMapping(GetLayoutBlockFlow(), data); |
| DCHECK(data->offset_mapping); |
| } |
| |
| return data->offset_mapping; |
| } |
| |
| void NGInlineNode::ComputeOffsetMapping(LayoutBlockFlow* layout_block_flow, |
| NGInlineNodeData* data) { |
| DCHECK(!data->offset_mapping); |
| DCHECK(!layout_block_flow->GetDocument().NeedsLayoutTreeUpdate() || |
| layout_block_flow->IsLayoutNGObjectForFormattedText()); |
| |
| const SvgTextChunkOffsets* chunk_offsets = nullptr; |
| if (data->svg_node_data_ && data->svg_node_data_->chunk_offsets.size() > 0) |
| chunk_offsets = &data->svg_node_data_->chunk_offsets; |
| |
| // TODO(xiaochengh): ComputeOffsetMappingIfNeeded() discards the |
| // NGInlineItems and text content built by |builder|, because they are |
| // already there in NGInlineNodeData. For efficiency, we should make |
| // |builder| not construct items and text content. |
| HeapVector<NGInlineItem> items; |
| ClearCollectionScope<HeapVector<NGInlineItem>> clear_scope(&items); |
| items.reserve(EstimateInlineItemsCount(*layout_block_flow)); |
| NGInlineItemsBuilderForOffsetMapping builder(layout_block_flow, &items, |
| chunk_offsets); |
| builder.GetOffsetMappingBuilder().ReserveCapacity( |
| EstimateOffsetMappingItemsCount(*layout_block_flow)); |
| CollectInlinesInternal(&builder, nullptr); |
| |
| // For non-NG object, we need the text, and also the inline items to resolve |
| // bidi levels. Otherwise |data| already has the text from the pre-layout |
| // phase, check they match. |
| if (data->text_content.IsNull()) { |
| DCHECK(!layout_block_flow->IsLayoutNGObject()); |
| data->text_content = builder.ToString(); |
| } else { |
| DCHECK(layout_block_flow->IsLayoutNGObject()); |
| } |
| |
| // TODO(xiaochengh): This doesn't compute offset mapping correctly when |
| // text-transform CSS property changes text length. |
| NGOffsetMappingBuilder& mapping_builder = builder.GetOffsetMappingBuilder(); |
| mapping_builder.SetDestinationString(data->text_content); |
| data->offset_mapping = mapping_builder.Build(); |
| DCHECK(data->offset_mapping); |
| } |
| |
| const NGOffsetMapping* NGInlineNode::GetOffsetMapping( |
| LayoutBlockFlow* layout_block_flow) { |
| DCHECK(!layout_block_flow->GetDocument().NeedsLayoutTreeUpdate()); |
| |
| if (UNLIKELY(layout_block_flow->NeedsLayout())) { |
| // TODO(kojii): This shouldn't happen, but is not easy to fix all cases. |
| // Return nullptr so that callers can chose to fail gracefully, or |
| // null-deref. crbug.com/946004 |
| return nullptr; |
| } |
| |
| NGInlineNode node(layout_block_flow); |
| CHECK(node.IsPrepareLayoutFinished()); |
| return node.ComputeOffsetMappingIfNeeded(); |
| } |
| |
| // Depth-first-scan of all LayoutInline and LayoutText nodes that make up this |
| // NGInlineNode object. Collects LayoutText items, merging them up into the |
| // parent LayoutInline where possible, and joining all text content in a single |
| // string to allow bidi resolution and shaping of the entire block. |
| void NGInlineNode::CollectInlines(NGInlineNodeData* data, |
| NGInlineNodeData* previous_data) const { |
| DCHECK(data->text_content.IsNull()); |
| DCHECK(data->items.empty()); |
| LayoutBlockFlow* block = GetLayoutBlockFlow(); |
| block->WillCollectInlines(); |
| |
| const SvgTextChunkOffsets* chunk_offsets = nullptr; |
| if (block->IsNGSVGText()) { |
| // SVG <text> doesn't support reusing the previous result now. |
| previous_data = nullptr; |
| data->svg_node_data_ = nullptr; |
| // We don't need to find text chunks if the IFC has only 0-1 character |
| // because of no Bidi reordering and no ligatures. |
| // This is an optimization for perf_tests/svg/France.html. |
| const auto* layout_text = DynamicTo<LayoutText>(block->FirstChild()); |
| bool empty_or_one_char = |
| !block->FirstChild() || (layout_text && !layout_text->NextSibling() && |
| layout_text->TextLength() <= 1); |
| if (!empty_or_one_char) |
| chunk_offsets = FindSvgTextChunks(*block, *data); |
| } |
| |
| data->items.reserve(EstimateInlineItemsCount(*block)); |
| NGInlineItemsBuilder builder(block, &data->items, chunk_offsets); |
| CollectInlinesInternal(&builder, previous_data); |
| if (block->IsNGSVGText() && !data->svg_node_data_) { |
| NGSvgTextLayoutAttributesBuilder svg_attr_builder(*this); |
| svg_attr_builder.Build(builder.ToString(), data->items); |
| data->svg_node_data_ = svg_attr_builder.CreateSvgInlineNodeData(); |
| } |
| builder.DidFinishCollectInlines(data); |
| |
| if (UNLIKELY(builder.HasUnicodeBidiPlainText())) |
| UseCounter::Count(GetDocument(), WebFeature::kUnicodeBidiPlainText); |
| } |
| |
| const SvgTextChunkOffsets* NGInlineNode::FindSvgTextChunks( |
| LayoutBlockFlow& block, |
| NGInlineNodeData& data) const { |
| TRACE_EVENT0("blink", "NGInlineNode::FindSvgTextChunks"); |
| // Build NGInlineItems and NGOffsetMapping first. They are used only by |
| // NGSVGTextLayoutAttributesBuilder, and are discarded because they might |
| // be different from final ones. |
| HeapVector<NGInlineItem> items; |
| ClearCollectionScope<HeapVector<NGInlineItem>> clear_scope(&items); |
| items.reserve(EstimateInlineItemsCount(block)); |
| NGInlineItemsBuilderForOffsetMapping items_builder(&block, &items); |
| NGOffsetMappingBuilder& mapping_builder = |
| items_builder.GetOffsetMappingBuilder(); |
| mapping_builder.ReserveCapacity(EstimateOffsetMappingItemsCount(block)); |
| CollectInlinesInternal(&items_builder, nullptr); |
| String ifc_text_content = items_builder.ToString(); |
| |
| NGSvgTextLayoutAttributesBuilder svg_attr_builder(*this); |
| svg_attr_builder.Build(ifc_text_content, items); |
| data.svg_node_data_ = svg_attr_builder.CreateSvgInlineNodeData(); |
| |
| // Compute DOM offsets of text chunks. |
| mapping_builder.SetDestinationString(ifc_text_content); |
| NGOffsetMapping* mapping = mapping_builder.Build(); |
| StringView ifc_text_view(ifc_text_content); |
| for (wtf_size_t i = 0; i < data.svg_node_data_->character_data_list.size(); |
| ++i) { |
| const std::pair<unsigned, NGSvgCharacterData>& char_data = |
| data.svg_node_data_->character_data_list[i]; |
| if (!char_data.second.anchored_chunk) |
| continue; |
| unsigned addressable_offset = char_data.first; |
| if (addressable_offset == 0u) |
| continue; |
| unsigned text_content_offset = svg_attr_builder.IfcTextContentOffsetAt(i); |
| const auto* unit = mapping->GetLastMappingUnit(text_content_offset); |
| DCHECK(unit); |
| auto result = data.svg_node_data_->chunk_offsets.insert( |
| To<LayoutText>(&unit->GetLayoutObject()), Vector<unsigned>()); |
| result.stored_value->value.push_back( |
| unit->ConvertTextContentToFirstDOMOffset(text_content_offset)); |
| } |
| return data.svg_node_data_->chunk_offsets.size() > 0 |
| ? &data.svg_node_data_->chunk_offsets |
| : nullptr; |
| } |
| |
| void NGInlineNode::SegmentText(NGInlineNodeData* data) const { |
| SegmentBidiRuns(data); |
| SegmentScriptRuns(data); |
| SegmentFontOrientation(data); |
| if (data->segments) |
| data->segments->ComputeItemIndex(data->items); |
| } |
| |
| // Segment NGInlineItem by script, Emoji, and orientation using RunSegmenter. |
| void NGInlineNode::SegmentScriptRuns(NGInlineNodeData* data) const { |
| String& text_content = data->text_content; |
| if (text_content.empty()) { |
| data->segments = nullptr; |
| return; |
| } |
| |
| if (text_content.Is8Bit() && !data->is_bidi_enabled_) { |
| if (data->items.size()) { |
| RunSegmenter::RunSegmenterRange range = { |
| 0u, data->text_content.length(), USCRIPT_LATIN, |
| OrientationIterator::kOrientationKeep, FontFallbackPriority::kText}; |
| NGInlineItem::SetSegmentData(range, &data->items); |
| } |
| data->segments = nullptr; |
| return; |
| } |
| |
| // Segment by script and Emoji. |
| // Orientation is segmented separately, because it may vary by items. |
| text_content.Ensure16Bit(); |
| RunSegmenter segmenter(text_content.Characters16(), text_content.length(), |
| FontOrientation::kHorizontal); |
| |
| RunSegmenter::RunSegmenterRange range = RunSegmenter::NullRange(); |
| bool consumed = segmenter.Consume(&range); |
| DCHECK(consumed); |
| if (range.end == text_content.length()) { |
| NGInlineItem::SetSegmentData(range, &data->items); |
| data->segments = nullptr; |
| return; |
| } |
| |
| // This node has multiple segments. |
| if (!data->segments) |
| data->segments = std::make_unique<NGInlineItemSegments>(); |
| data->segments->ComputeSegments(&segmenter, &range); |
| DCHECK_EQ(range.end, text_content.length()); |
| } |
| |
| void NGInlineNode::SegmentFontOrientation(NGInlineNodeData* data) const { |
| // Segment by orientation, only if vertical writing mode and items with |
| // 'text-orientation: mixed'. |
| if (GetLayoutBlockFlow()->IsHorizontalWritingMode()) |
| return; |
| |
| HeapVector<NGInlineItem>& items = data->items; |
| if (items.empty()) |
| return; |
| String& text_content = data->text_content; |
| text_content.Ensure16Bit(); |
| |
| // If we don't have |NGInlineItemSegments| yet, create a segment for the |
| // entire content. |
| const unsigned capacity = items.size() + text_content.length() / 10; |
| NGInlineItemSegments* segments = data->segments.get(); |
| if (segments) { |
| DCHECK(!data->segments->IsEmpty()); |
| data->segments->ReserveCapacity(capacity); |
| DCHECK_EQ(text_content.length(), data->segments->EndOffset()); |
| } |
| unsigned segment_index = 0; |
| |
| for (const NGInlineItem& item : items) { |
| if (item.Type() == NGInlineItem::kText && item.Length() && |
| item.Style()->GetFont().GetFontDescription().Orientation() == |
| FontOrientation::kVerticalMixed) { |
| if (!segments) { |
| data->segments = std::make_unique<NGInlineItemSegments>(); |
| segments = data->segments.get(); |
| segments->ReserveCapacity(capacity); |
| segments->Append(text_content.length(), item); |
| DCHECK_EQ(text_content.length(), data->segments->EndOffset()); |
| } |
| segment_index = segments->AppendMixedFontOrientation( |
| text_content, item.StartOffset(), item.EndOffset(), segment_index); |
| } |
| } |
| } |
| |
| // Segment bidi runs by resolving bidi embedding levels. |
| // http://unicode.org/reports/tr9/#Resolving_Embedding_Levels |
| void NGInlineNode::SegmentBidiRuns(NGInlineNodeData* data) const { |
| if (!data->is_bidi_enabled_) { |
| data->SetBaseDirection(TextDirection::kLtr); |
| return; |
| } |
| |
| NGBidiParagraph bidi; |
| data->text_content.Ensure16Bit(); |
| if (!bidi.SetParagraph(data->text_content, Style())) { |
| // On failure, give up bidi resolving and reordering. |
| data->is_bidi_enabled_ = false; |
| data->SetBaseDirection(TextDirection::kLtr); |
| return; |
| } |
| |
| data->SetBaseDirection(bidi.BaseDirection()); |
| |
| if (bidi.IsUnidirectional() && IsLtr(bidi.BaseDirection())) { |
| // All runs are LTR, no need to reorder. |
| data->is_bidi_enabled_ = false; |
| return; |
| } |
| |
| HeapVector<NGInlineItem>& items = data->items; |
| unsigned item_index = 0; |
| for (unsigned start = 0; start < data->text_content.length();) { |
| UBiDiLevel level; |
| unsigned end = bidi.GetLogicalRun(start, &level); |
| DCHECK_EQ(items[item_index].start_offset_, start); |
| item_index = NGInlineItem::SetBidiLevel(items, item_index, end, level); |
| start = end; |
| } |
| #if DCHECK_IS_ON() |
| // Check all items have bidi levels, except trailing non-length items. |
| // Items that do not create break opportunities such as kOutOfFlowPositioned |
| // do not have corresponding characters, and that they do not have bidi level |
| // assigned. |
| while (item_index < items.size() && !items[item_index].Length()) |
| item_index++; |
| DCHECK_EQ(item_index, items.size()); |
| #endif |
| } |
| |
| void NGInlineNode::ShapeText(NGInlineItemsData* data, |
| const String* previous_text, |
| const HeapVector<NGInlineItem>* previous_items, |
| const Font* override_font) const { |
| TRACE_EVENT0("fonts", "NGInlineNode::ShapeText"); |
| const String& text_content = data->text_content; |
| HeapVector<NGInlineItem>* items = &data->items; |
| |
| ShapeResultSpacing<String> spacing(text_content, IsSvgText()); |
| |
| // For consistency with similar usages of ShapeCache (e.g. canvas) and in |
| // order to avoid caching bugs (e.g. with scripts having Arabic joining) |
| // NGShapeCache is only enabled when the IFC is made of a single text item. To |
| // be efficient, NGShapeCache only stores entries for short strings and |
| // without memory copy, so don't allow it if the text item is too long or if |
| // the start/end offsets match a substring. Don't allow it either if a call to |
| // ApplySpacing is needed to avoid a costly copy of the ShapeResult in the |
| // loop below. |
| auto ShapeCacheAllowedFor = [&override_font, &spacing, |
| &text_content](const NGInlineItem& single_item) { |
| if (!(single_item.Type() == NGInlineItem::kText && |
| single_item.StartOffset() == 0 && |
| single_item.EndOffset() == text_content.length())) { |
| return false; |
| } |
| const Font& font = |
| override_font ? *override_font : single_item.FontWithSvgScaling(); |
| return !spacing.SetSpacing(font.GetFontDescription()); |
| }; |
| |
| const bool allow_shape_cache = |
| RuntimeEnabledFeatures::LayoutNGShapeCacheEnabled() && |
| text_content.length() <= NGShapeCache::MaxTextLengthOfEntries() && |
| items->size() == 1 && ShapeCacheAllowedFor((*items)[0]); |
| |
| // Provide full context of the entire node to the shaper. |
| ReusingTextShaper shaper(data, previous_items, allow_shape_cache); |
| |
| DCHECK(!data->segments || |
| data->segments->EndOffset() == text_content.length()); |
| |
| for (unsigned index = 0; index < items->size();) { |
| NGInlineItem& start_item = (*items)[index]; |
| if (start_item.Type() != NGInlineItem::kText || !start_item.Length()) { |
| index++; |
| continue; |
| } |
| |
| const ComputedStyle& start_style = *start_item.Style(); |
| const Font& font = |
| override_font ? *override_font : start_item.FontWithSvgScaling(); |
| #if DCHECK_IS_ON() |
| if (!IsTextCombine()) { |
| DCHECK(!override_font); |
| } else { |
| DCHECK_EQ(font.GetFontDescription().Orientation(), |
| FontOrientation::kHorizontal); |
| LayoutNGTextCombine::AssertStyleIsValid(start_style); |
| DCHECK(!override_font || |
| font.GetFontDescription().WidthVariant() != kRegularWidth); |
| } |
| #endif |
| TextDirection direction = start_item.Direction(); |
| unsigned end_index = index + 1; |
| unsigned end_offset = start_item.EndOffset(); |
| |
| // Symbol marker is painted as graphics. Create a ShapeResult of space |
| // glyphs with the desired size to make it less special for line breaker. |
| if (UNLIKELY(start_item.IsSymbolMarker())) { |
| LayoutUnit symbol_width = ListMarker::WidthOfSymbol( |
| start_style, |
| LayoutCounter::ListStyle(start_item.GetLayoutObject(), start_style)); |
| DCHECK_GE(symbol_width, 0); |
| start_item.shape_result_ = ShapeResult::CreateForSpaces( |
| &font, direction, start_item.StartOffset(), start_item.Length(), |
| symbol_width); |
| index++; |
| continue; |
| } |
| |
| // Scan forward until an item is encountered that should trigger a shaping |
| // break. This ensures that adjacent text items are shaped together whenever |
| // possible as this is required for accurate cross-element shaping. |
| unsigned num_text_items = 1; |
| for (; end_index < items->size(); end_index++) { |
| const NGInlineItem& item = (*items)[end_index]; |
| |
| if (item.Type() == NGInlineItem::kControl) { |
| // Do not shape across control characters (line breaks, zero width |
| // spaces, etc). |
| break; |
| } |
| if (item.Type() == NGInlineItem::kText) { |
| if (!item.Length()) |
| continue; |
| if (item.TextType() == NGTextType::kSymbolMarker) { |
| break; |
| } |
| if (ShouldBreakShapingBeforeText(item, start_item, start_style, font, |
| direction)) { |
| break; |
| } |
| // Break shaping at ZWNJ so that it prevents kerning. ZWNJ is always at |
| // the beginning of an item for this purpose; see NGInlineItemsBuilder. |
| if (text_content[item.StartOffset()] == kZeroWidthNonJoinerCharacter) |
| break; |
| end_offset = item.EndOffset(); |
| num_text_items++; |
| } else if (item.Type() == NGInlineItem::kOpenTag) { |
| if (ShouldBreakShapingBeforeBox(item)) |
| break; |
| // Should not have any characters to be opaque to shaping. |
| DCHECK_EQ(0u, item.Length()); |
| } else if (item.Type() == NGInlineItem::kCloseTag) { |
| if (ShouldBreakShapingAfterBox(item)) |
| break; |
| // Should not have any characters to be opaque to shaping. |
| DCHECK_EQ(0u, item.Length()); |
| } else { |
| break; |
| } |
| } |
| |
| // Shaping a single item. Skip if the existing results remain valid. |
| if (previous_text && end_offset == start_item.EndOffset() && |
| !NeedsShaping(start_item) && LIKELY(!IsTextCombine())) { |
| DCHECK_EQ(start_item.StartOffset(), |
| start_item.TextShapeResult()->StartIndex()); |
| DCHECK_EQ(start_item.EndOffset(), |
| start_item.TextShapeResult()->EndIndex()); |
| index++; |
| continue; |
| } |
| |
| // Results may only be reused if all items in the range remain valid. |
| if (previous_text) { |
| bool has_valid_shape_results = true; |
| for (unsigned item_index = index; item_index < end_index; item_index++) { |
| if (NeedsShaping((*items)[item_index])) { |
| has_valid_shape_results = false; |
| break; |
| } |
| } |
| |
| // When shaping across multiple items checking whether the individual |
| // items has valid shape results isn't sufficient as items may have been |
| // re-ordered or removed. |
| // TODO(layout-dev): It would probably be faster to check for removed or |
| // moved items but for now comparing the string itself will do. |
| unsigned text_start = start_item.StartOffset(); |
| DCHECK_GE(end_offset, text_start); |
| unsigned text_length = end_offset - text_start; |
| if (has_valid_shape_results && previous_text && |
| end_offset <= previous_text->length() && |
| StringView(text_content, text_start, text_length) == |
| StringView(*previous_text, text_start, text_length)) { |
| index = end_index; |
| continue; |
| } |
| } |
| |
| // Shape each item with the full context of the entire node. |
| scoped_refptr<const ShapeResult> shape_result = |
| shaper.Shape(start_item, font, end_offset); |
| |
| if (UNLIKELY(spacing.SetSpacing(font.GetFontDescription()))) { |
| DCHECK(!IsTextCombine()) << GetLayoutBlockFlow(); |
| DCHECK(!allow_shape_cache); |
| // The ShapeResult is actually not a reusable entry of NGShapeCache, |
| // so it is safe to mutate it. |
| const_cast<ShapeResult*>(shape_result.get())->ApplySpacing(spacing); |
| } |
| |
| // If the text is from one item, use the ShapeResult as is. |
| if (end_offset == start_item.EndOffset()) { |
| start_item.shape_result_ = std::move(shape_result); |
| DCHECK_EQ(start_item.TextShapeResult()->StartIndex(), |
| start_item.StartOffset()); |
| DCHECK_EQ(start_item.TextShapeResult()->EndIndex(), |
| start_item.EndOffset()); |
| index++; |
| continue; |
| } |
| |
| // If the text is from multiple items, split the ShapeResult to |
| // corresponding items. |
| DCHECK_GT(num_text_items, 0u); |
| // "32" is heuristic, most major sites are up to 8 or so, wikipedia is 21. |
| Vector<ShapeResult::ShapeRange, 32> text_item_ranges; |
| text_item_ranges.ReserveInitialCapacity(num_text_items); |
| const bool has_ligatures = |
| shape_result->NumGlyphs() < shape_result->NumCharacters(); |
| if (has_ligatures) { |
| shape_result->EnsurePositionData(); |
| } |
| for (; index < end_index; index++) { |
| NGInlineItem& item = (*items)[index]; |
| if (item.Type() != NGInlineItem::kText || !item.Length()) |
| continue; |
| |
| // We don't use SafeToBreak API here because this is not a line break. |
| // The ShapeResult is broken into multiple results, but they must look |
| // like they were not broken. |
| // |
| // When multiple code units shape to one glyph, such as ligatures, the |
| // item that has its first code unit keeps the glyph. |
| scoped_refptr<ShapeResult> item_result = |
| ShapeResult::CreateEmpty(*shape_result.get()); |
| text_item_ranges.emplace_back(item.StartOffset(), item.EndOffset(), |
| item_result.get()); |
| if (has_ligatures && item.EndOffset() < shape_result->EndIndex() && |
| shape_result->CachedNextSafeToBreakOffset(item.EndOffset()) != |
| item.EndOffset()) { |
| // Note: We should not reuse `ShapeResult` ends with ligature glyph. |
| // e.g. <div>f<span>i</div> to <div>f</div> with ligature "fi". |
| // See http://crbug.com/1409702 |
| item.SetUnsafeToReuseShapeResult(); |
| } |
| item.shape_result_ = std::move(item_result); |
| } |
| DCHECK_EQ(text_item_ranges.size(), num_text_items); |
| shape_result->CopyRanges(text_item_ranges.data(), text_item_ranges.size()); |
| } |
| |
| #if DCHECK_IS_ON() |
| for (const NGInlineItem& item : *items) { |
| if (item.Type() == NGInlineItem::kText && item.Length()) { |
| DCHECK(item.TextShapeResult()); |
| DCHECK_EQ(item.TextShapeResult()->StartIndex(), item.StartOffset()); |
| DCHECK_EQ(item.TextShapeResult()->EndIndex(), item.EndOffset()); |
| } |
| } |
| #endif |
| } |
| |
| // Create HeapVector<NGInlineItem> with :first-line rules applied if needed. |
| void NGInlineNode::ShapeTextForFirstLineIfNeeded(NGInlineNodeData* data) const { |
| // First check if the document has any :first-line rules. |
| DCHECK(!data->first_line_items_); |
| LayoutObject* layout_object = GetLayoutBox(); |
| if (!layout_object->GetDocument().GetStyleEngine().UsesFirstLineRules()) |
| return; |
| |
| // Check if :first-line rules make any differences in the style. |
| const ComputedStyle* block_style = layout_object->Style(); |
| const ComputedStyle* first_line_style = layout_object->FirstLineStyle(); |
| if (block_style == first_line_style) |
| return; |
| |
| auto* first_line_items = MakeGarbageCollected<NGInlineItemsData>(); |
| first_line_items->text_content = data->text_content; |
| bool needs_reshape = false; |
| if (first_line_style->TextTransform() != block_style->TextTransform()) { |
| // TODO(kojii): This logic assumes that text-transform is applied only to |
| // ::first-line, and does not work when the base style has text-transform |
| // and ::first-line has different text-transform. |
| first_line_style->ApplyTextTransform(&first_line_items->text_content); |
| if (first_line_items->text_content != data->text_content) { |
| // TODO(kojii): When text-transform changes the length, we need to adjust |
| // offset in NGInlineItem, or re-collect inlines. Other classes such as |
| // line breaker need to support the scenario too. For now, we force the |
| // string to be the same length to prevent them from crashing. This may |
| // result in a missing or a duplicate character if the length changes. |
| TruncateOrPadText(&first_line_items->text_content, |
| data->text_content.length()); |
| needs_reshape = true; |
| } |
| } |
| |
| first_line_items->items.AppendVector(data->items); |
| for (auto& item : first_line_items->items) { |
| item.SetStyleVariant(NGStyleVariant::kFirstLine); |
| } |
| |
| // Re-shape if the font is different. |
| if (needs_reshape || FirstLineNeedsReshape(*first_line_style, *block_style)) |
| ShapeText(first_line_items); |
| |
| data->first_line_items_ = first_line_items; |
| } |
| |
| void NGInlineNode::ShapeTextIncludingFirstLine( |
| NGInlineNodeData* data, |
| const String* previous_text, |
| const HeapVector<NGInlineItem>* previous_items) const { |
| ShapeText(data, previous_text, previous_items); |
| ShapeTextForFirstLineIfNeeded(data); |
| } |
| |
| void NGInlineNode::AssociateItemsWithInlines(NGInlineNodeData* data) const { |
| #if DCHECK_IS_ON() |
| HeapHashSet<Member<LayoutObject>> associated_objects; |
| #endif |
| HeapVector<NGInlineItem>& items = data->items; |
| WTF::wtf_size_t size = items.size(); |
| for (WTF::wtf_size_t i = 0; i != size;) { |
| LayoutObject* object = items[i].GetLayoutObject(); |
| auto* layout_text = DynamicTo<LayoutText>(object); |
| if (layout_text && !layout_text->IsBR()) { |
| #if DCHECK_IS_ON() |
| // Items split from a LayoutObject should be consecutive. |
| DCHECK(associated_objects.insert(object).is_new_entry); |
| #endif |
| layout_text->ClearHasBidiControlInlineItems(); |
| bool has_bidi_control = false; |
| WTF::wtf_size_t begin = i; |
| for (++i; i != size; ++i) { |
| auto& item = items[i]; |
| if (item.GetLayoutObject() != object) |
| break; |
| if (item.Type() == NGInlineItem::kBidiControl) |
| has_bidi_control = true; |
| } |
| layout_text->SetInlineItems(data, begin, i - begin); |
| if (has_bidi_control) |
| layout_text->SetHasBidiControlInlineItems(); |
| continue; |
| } |
| ++i; |
| } |
| } |
| |
| const NGLayoutResult* NGInlineNode::Layout( |
| const NGConstraintSpace& constraint_space, |
| const NGBreakToken* break_token, |
| const NGColumnSpannerPath* column_spanner_path, |
| NGInlineChildLayoutContext* context) const { |
| PrepareLayoutIfNeeded(); |
| |
| const auto* inline_break_token = To<NGInlineBreakToken>(break_token); |
| NGInlineLayoutAlgorithm algorithm(*this, constraint_space, inline_break_token, |
| column_spanner_path, context); |
| return algorithm.Layout(); |
| } |
| |
| namespace { |
| |
| template <typename CharType> |
| String CreateTextContentForStickyImagesQuirk( |
| const CharType* text, |
| unsigned length, |
| base::span<const NGInlineItem> items) { |
| StringBuffer<CharType> buffer(length); |
| CharType* characters = buffer.Characters(); |
| memcpy(characters, text, length * sizeof(CharType)); |
| for (const NGInlineItem& item : items) { |
| if (item.Type() == NGInlineItem::kAtomicInline && item.IsImage()) { |
| DCHECK_EQ(characters[item.StartOffset()], kObjectReplacementCharacter); |
| characters[item.StartOffset()] = kNoBreakSpaceCharacter; |
| } |
| } |
| return buffer.Release(); |
| } |
| |
| } // namespace |
| |
| // The stick images quirk changes the line breaking behavior around images. This |
| // function returns a text content that has non-breaking spaces for images, so |
| // that no changes are needed in the line breaking logic. |
| // https://quirks.spec.whatwg.org/#the-table-cell-width-calculation-quirk |
| // static |
| String NGInlineNode::TextContentForStickyImagesQuirk( |
| const NGInlineItemsData& items_data) { |
| const String& text_content = items_data.text_content; |
| for (const NGInlineItem& item : items_data.items) { |
| if (item.Type() == NGInlineItem::kAtomicInline && item.IsImage()) { |
| if (text_content.Is8Bit()) { |
| return CreateTextContentForStickyImagesQuirk( |
| text_content.Characters8(), text_content.length(), |
| base::span<const NGInlineItem>(&item, items_data.items.end())); |
| } |
| return CreateTextContentForStickyImagesQuirk( |
| text_content.Characters16(), text_content.length(), |
| base::span<const NGInlineItem>(&item, items_data.items.end())); |
| } |
| } |
| return text_content; |
| } |
| |
| static LayoutUnit ComputeContentSize( |
| NGInlineNode node, |
| WritingMode container_writing_mode, |
| const NGConstraintSpace& space, |
| const MinMaxSizesFloatInput& float_input, |
| NGLineBreakerMode mode, |
| NGLineBreaker::MaxSizeCache* max_size_cache, |
| absl::optional<LayoutUnit>* max_size_out, |
| bool* depends_on_block_constraints_out) { |
| const ComputedStyle& style = node.Style(); |
| LayoutUnit available_inline_size = |
| mode == NGLineBreakerMode::kMaxContent ? LayoutUnit::Max() : LayoutUnit(); |
| |
| NGExclusionSpace empty_exclusion_space; |
| NGPositionedFloatVector empty_leading_floats; |
| NGLineLayoutOpportunity line_opportunity(available_inline_size); |
| LayoutUnit result; |
| NGLineBreaker line_breaker( |
| node, mode, space, line_opportunity, empty_leading_floats, |
| /* handled_leading_floats_index */ 0u, /* break_token */ nullptr, |
| /* column_spanner_path */ nullptr, &empty_exclusion_space); |
| line_breaker.SetIntrinsicSizeOutputs(max_size_cache, |
| depends_on_block_constraints_out); |
| const NGInlineItemsData& items_data = line_breaker.ItemsData(); |
| |
| // Computes max-size for floats in inline formatting context. |
| class FloatsMaxSize { |
| STACK_ALLOCATED(); |
| |
| public: |
| explicit FloatsMaxSize(const MinMaxSizesFloatInput& float_input) |
| : floats_inline_size_(float_input.float_left_inline_size + |
| float_input.float_right_inline_size) { |
| DCHECK_GE(floats_inline_size_, 0); |
| } |
| |
| void AddFloat(const ComputedStyle& float_style, |
| const ComputedStyle& style, |
| LayoutUnit float_inline_max_size_with_margin) { |
| floating_objects_.push_back(NGInlineNode::FloatingObject{ |
| float_style, style, float_inline_max_size_with_margin}); |
| } |
| |
| LayoutUnit ComputeMaxSizeForLine(LayoutUnit line_inline_size, |
| LayoutUnit max_inline_size) { |
| if (floating_objects_.empty()) |
| return std::max(max_inline_size, line_inline_size); |
| |
| EFloat previous_float_type = EFloat::kNone; |
| for (const auto& floating_object : floating_objects_) { |
| const EClear float_clear = |
| floating_object.float_style.Clear(floating_object.style); |
| |
| // If this float clears the previous float we start a new "line". |
| // This is subtly different to block layout which will only reset either |
| // the left or the right float size trackers. |
| if ((previous_float_type == EFloat::kLeft && |
| (float_clear == EClear::kBoth || float_clear == EClear::kLeft)) || |
| (previous_float_type == EFloat::kRight && |
| (float_clear == EClear::kBoth || float_clear == EClear::kRight))) { |
| max_inline_size = |
| std::max(max_inline_size, line_inline_size + floats_inline_size_); |
| floats_inline_size_ = LayoutUnit(); |
| } |
| |
| // When negative margins move the float outside the content area, |
| // such float should not affect the content size. |
| floats_inline_size_ += floating_object.float_inline_max_size_with_margin |
| .ClampNegativeToZero(); |
| previous_float_type = |
| floating_object.float_style.Floating(floating_object.style); |
| } |
| max_inline_size = |
| std::max(max_inline_size, line_inline_size + floats_inline_size_); |
| floats_inline_size_ = LayoutUnit(); |
| floating_objects_.Shrink(0); |
| return max_inline_size; |
| } |
| |
| private: |
| LayoutUnit floats_inline_size_; |
| HeapVector<NGInlineNode::FloatingObject, 4> floating_objects_; |
| }; |
| |
| // This struct computes the max size from the line break results for the min |
| // size. |
| struct MaxSizeFromMinSize { |
| STACK_ALLOCATED(); |
| |
| public: |
| LayoutUnit position; |
| LayoutUnit max_size; |
| const NGInlineItemsData& items_data; |
| const NGInlineItem* next_item; |
| const NGLineBreaker::MaxSizeCache& max_size_cache; |
| FloatsMaxSize* floats; |
| bool is_after_break = true; |
| |
| explicit MaxSizeFromMinSize( |
| const NGInlineItemsData& items_data, |
| const NGLineBreaker::MaxSizeCache& max_size_cache, |
| FloatsMaxSize* floats) |
| : items_data(items_data), |
| next_item(items_data.items.begin()), |
| max_size_cache(max_size_cache), |
| floats(floats) {} |
| |
| // Add all text items up to |end|. The line break results for min size |
| // may break text into multiple lines, and may remove trailing spaces. For |
| // max size, use the original text widths from NGInlineItem instead. |
| void AddTextUntil(const NGInlineItem* end) { |
| DCHECK(end); |
| for (; next_item != end; ++next_item) { |
| if (next_item->Type() == NGInlineItem::kText && next_item->Length()) { |
| DCHECK(next_item->TextShapeResult()); |
| const ShapeResult& shape_result = *next_item->TextShapeResult(); |
| position += shape_result.SnappedWidth().ClampNegativeToZero(); |
| } |
| } |
| } |
| |
| void ForceLineBreak(const NGLineInfo& line_info) { |
| // Add all text up to the end of the line. There may be spaces that were |
| // removed during the line breaking. |
| CHECK_LE(line_info.EndItemIndex(), items_data.items.size()); |
| AddTextUntil(items_data.items.begin() + line_info.EndItemIndex()); |
| max_size = floats->ComputeMaxSizeForLine(position.ClampNegativeToZero(), |
| max_size); |
| position = LayoutUnit(); |
| is_after_break = true; |
| } |
| |
| void AddTabulationCharacters(const NGInlineItem& item, unsigned length) { |
| DCHECK_GE(length, 1u); |
| AddTextUntil(&item); |
| DCHECK(item.Style()); |
| const ComputedStyle& style = *item.Style(); |
| const Font& font = style.GetFont(); |
| const SimpleFontData* font_data = font.PrimaryFont(); |
| const TabSize& tab_size = style.GetTabSize(); |
| float advance = font.TabWidth(font_data, tab_size, position); |
| DCHECK_GE(length, 1u); |
| if (length > 1u) |
| advance += font.TabWidth(font_data, tab_size) * (length - 1); |
| position += LayoutUnit::FromFloatCeil(advance).ClampNegativeToZero(); |
| } |
| |
| LayoutUnit Finish(const NGInlineItem* end) { |
| AddTextUntil(end); |
| return floats->ComputeMaxSizeForLine(position.ClampNegativeToZero(), |
| max_size); |
| } |
| |
| void ComputeFromMinSize(const NGLineInfo& line_info) { |
| if (is_after_break) { |
| position += line_info.TextIndent(); |
| is_after_break = false; |
| } |
| |
| for (const NGInlineItemResult& result : line_info.Results()) { |
| const NGInlineItem& item = *result.item; |
| if (item.Type() == NGInlineItem::kText) { |
| // Text in NGInlineItemResult may be wrapped and trailing spaces |
| // may be removed. Ignore them, but add text later from |
| // NGInlineItem. |
| continue; |
| } |
| #if DCHECK_IS_ON() |
| if (item.Type() == NGInlineItem::kBlockInInline) |
| DCHECK(line_info.HasForcedBreak()); |
| #endif |
| if (item.Type() == NGInlineItem::kAtomicInline || |
| item.Type() == NGInlineItem::kBlockInInline) { |
| // The max-size for atomic inlines are cached in |max_size_cache|. |
| unsigned item_index = |
| base::checked_cast<unsigned>(&item - items_data.items.begin()); |
| position += max_size_cache[item_index]; |
| continue; |
| } |
| if (item.Type() == NGInlineItem::kControl) { |
| UChar c = items_data.text_content[item.StartOffset()]; |
| #if DCHECK_IS_ON() |
| if (c == kNewlineCharacter) |
| DCHECK(line_info.HasForcedBreak()); |
| #endif |
| // Tabulation characters change the widths by their positions, so |
| // their widths for the max size may be different from the widths for |
| // the min size. Fall back to 2 pass for now. |
| if (c == kTabulationCharacter) { |
| AddTabulationCharacters(item, result.Length()); |
| continue; |
| } |
| } |
| position += result.inline_size; |
| } |
| // Compute the forced break after all results were handled, because |
| // when close tags appear after a forced break, they are included in |
| // the line, and they may have inline sizes. crbug.com/991320. |
| if (line_info.HasForcedBreak()) |
| ForceLineBreak(line_info); |
| } |
| }; |
| |
| if (UNLIKELY(node.IsInitialLetterBox())) { |
| LayoutUnit inline_size = LayoutUnit(); |
| NGLineInfo line_info; |
| do { |
| line_breaker.NextLine(&line_info); |
| if (line_info.Results().empty()) |
| break; |
| inline_size = |
| std::max(CalculateInitialLetterBoxInlineSize(line_info), inline_size); |
| } while (!line_breaker.IsFinished()); |
| return inline_size; |
| } |
| |
| FloatsMaxSize floats_max_size(float_input); |
| bool can_compute_max_size_from_min_size = true; |
| MaxSizeFromMinSize max_size_from_min_size(items_data, *max_size_cache, |
| &floats_max_size); |
| |
| NGLineInfo line_info; |
| do { |
| line_breaker.NextLine(&line_info); |
| if (line_info.Results().empty()) |
| break; |
| |
| LayoutUnit inline_size = line_info.Width(); |
| for (const NGInlineItemResult& item_result : line_info.Results()) { |
| DCHECK(item_result.item); |
| const NGInlineItem& item = *item_result.item; |
| if (item.Type() != NGInlineItem::kFloating) |
| continue; |
| LayoutObject* floating_object = item.GetLayoutObject(); |
| DCHECK(floating_object && floating_object->IsFloating()); |
| |
| NGBlockNode float_node(To<LayoutBox>(floating_object)); |
| |
| NGMinMaxConstraintSpaceBuilder builder(space, style, float_node, |
| /* is_new_fc */ true); |
| builder.SetAvailableBlockSize(space.AvailableSize().block_size); |
| builder.SetPercentageResolutionBlockSize( |
| space.PercentageResolutionBlockSize()); |
| builder.SetReplacedPercentageResolutionBlockSize( |
| space.ReplacedPercentageResolutionBlockSize()); |
| const auto float_space = builder.ToConstraintSpace(); |
| |
| const MinMaxSizesResult child_result = |
| ComputeMinAndMaxContentContribution(style, float_node, float_space); |
| LayoutUnit child_inline_margins = |
| ComputeMarginsFor(float_space, float_node.Style(), space).InlineSum(); |
| |
| if (depends_on_block_constraints_out) { |
| *depends_on_block_constraints_out |= |
| child_result.depends_on_block_constraints; |
| } |
| |
| if (mode == NGLineBreakerMode::kMinContent) { |
| result = std::max(result, |
| child_result.sizes.min_size + child_inline_margins); |
| } |
| floats_max_size.AddFloat( |
| float_node.Style(), style, |
| child_result.sizes.max_size + child_inline_margins); |
| } |
| |
| if (mode == NGLineBreakerMode::kMinContent) { |
| result = std::max(result, inline_size); |
| can_compute_max_size_from_min_size = |
| can_compute_max_size_from_min_size && |
| // `box-decoration-break: clone` clones box decorations to each |
| // fragment (line) that we cannot compute max-content from |
| // min-content. |
| !line_breaker.HasClonedBoxDecorations(); |
| if (can_compute_max_size_from_min_size) |
| max_size_from_min_size.ComputeFromMinSize(line_info); |
| } else { |
| result = floats_max_size.ComputeMaxSizeForLine(inline_size, result); |
| } |
| } while (!line_breaker.IsFinished()); |
| |
| if (mode == NGLineBreakerMode::kMinContent && |
| can_compute_max_size_from_min_size) { |
| if (node.IsSvgText()) { |
| *max_size_out = result; |
| return result; |
| // The following DCHECK_EQ() doesn't work well for SVG <text> because |
| // it has glyph-split NGInlineItemResults. The sum of NGInlineItem |
| // widths and the sum of NGInlineItemResult widths can be different. |
| } |
| *max_size_out = max_size_from_min_size.Finish(items_data.items.end()); |
| // Check the max size matches to the value computed from 2 pass. |
| #if DCHECK_IS_ON() |
| LayoutUnit content_size = ComputeContentSize( |
| node, container_writing_mode, space, float_input, |
| NGLineBreakerMode::kMaxContent, max_size_cache, nullptr, nullptr); |
| bool values_might_be_saturated = |
| (*max_size_out)->MightBeSaturated() || content_size.MightBeSaturated(); |
| if (!values_might_be_saturated) { |
| DCHECK_EQ((*max_size_out)->Round(), content_size.Round()) |
| << node.GetLayoutBox(); |
| } |
| #endif |
| } |
| |
| return result; |
| } |
| |
| MinMaxSizesResult NGInlineNode::ComputeMinMaxSizes( |
| WritingMode container_writing_mode, |
| const NGConstraintSpace& space, |
| const MinMaxSizesFloatInput& float_input) const { |
| PrepareLayoutIfNeeded(); |
| |
| // Compute the max of inline sizes of all line boxes with 0 available inline |
| // size. This gives the min-content, the width where lines wrap at every |
| // break opportunity. |
| NGLineBreaker::MaxSizeCache max_size_cache; |
| MinMaxSizes sizes; |
| absl::optional<LayoutUnit> max_size; |
| bool depends_on_block_constraints = false; |
| sizes.min_size = |
| ComputeContentSize(*this, container_writing_mode, space, float_input, |
| NGLineBreakerMode::kMinContent, &max_size_cache, |
| &max_size, &depends_on_block_constraints); |
| if (max_size) { |
| sizes.max_size = *max_size; |
| } else { |
| sizes.max_size = ComputeContentSize( |
| *this, container_writing_mode, space, float_input, |
| NGLineBreakerMode::kMaxContent, &max_size_cache, nullptr, nullptr); |
| } |
| |
| // Negative text-indent can make min > max. Ensure min is the minimum size. |
| sizes.min_size = std::min(sizes.min_size, sizes.max_size); |
| |
| return MinMaxSizesResult(sizes, depends_on_block_constraints); |
| } |
| |
| bool NGInlineNode::UseFirstLineStyle() const { |
| return GetLayoutBox() && |
| GetLayoutBox()->GetDocument().GetStyleEngine().UsesFirstLineRules(); |
| } |
| |
| void NGInlineNode::CheckConsistency() const { |
| #if DCHECK_IS_ON() |
| const HeapVector<NGInlineItem>& items = Data().items; |
| for (const NGInlineItem& item : items) { |
| DCHECK(!item.GetLayoutObject() || !item.Style() || |
| item.Style() == item.GetLayoutObject()->Style()); |
| } |
| #endif |
| } |
| |
| const Vector<std::pair<unsigned, NGSvgCharacterData>>& |
| NGInlineNode::SvgCharacterDataList() const { |
| DCHECK(IsSvgText()); |
| return Data().svg_node_data_->character_data_list; |
| } |
| |
| const HeapVector<SvgTextContentRange>& NGInlineNode::SvgTextLengthRangeList() |
| const { |
| DCHECK(IsSvgText()); |
| return Data().svg_node_data_->text_length_range_list; |
| } |
| |
| const HeapVector<SvgTextContentRange>& NGInlineNode::SvgTextPathRangeList() |
| const { |
| DCHECK(IsSvgText()); |
| return Data().svg_node_data_->text_path_range_list; |
| } |
| |
| void NGInlineNode::AdjustFontForTextCombineUprightAll() const { |
| DCHECK(IsTextCombine()) << GetLayoutBlockFlow(); |
| DCHECK(IsPrepareLayoutFinished()) << GetLayoutBlockFlow(); |
| |
| const float content_width = CalculateWidthForTextCombine(ItemsData(false)); |
| if (UNLIKELY(content_width == 0.0f)) |
| return; // See "fast/css/zero-font-size-crash.html". |
| auto& text_combine = *To<LayoutNGTextCombine>(GetLayoutBlockFlow()); |
| const float desired_width = text_combine.DesiredWidth(); |
| text_combine.ResetLayout(); |
| if (UNLIKELY(desired_width == 0.0f)) { |
| // See http://crbug.com/1342520 |
| return; |
| } |
| if (content_width <= desired_width) |
| return; |
| |
| const Font& font = Style().GetFont(); |
| FontSelector* const font_selector = font.GetFontSelector(); |
| FontDescription description = font.GetFontDescription(); |
| |
| // Try compressed fonts. |
| static const std::array<FontWidthVariant, 3> kWidthVariants = { |
| kHalfWidth, kThirdWidth, kQuarterWidth}; |
| for (const auto width_variant : kWidthVariants) { |
| description.SetWidthVariant(width_variant); |
| Font compressed_font(description, font_selector); |
| ShapeText(MutableData(), nullptr, nullptr, &compressed_font); |
| if (CalculateWidthForTextCombine(ItemsData(false)) <= desired_width) { |
| text_combine.SetCompressedFont(compressed_font); |
| return; |
| } |
| } |
| |
| // There is no compressed font to fit within 1em. We use original font with |
| // scaling. |
| ShapeText(MutableData()); |
| DCHECK_EQ(content_width, CalculateWidthForTextCombine(ItemsData(false))); |
| DCHECK_NE(content_width, 0.0f); |
| text_combine.SetScaleX(desired_width / content_width); |
| } |
| |
| bool NGInlineNode::NeedsShapingForTesting(const NGInlineItem& item) { |
| return NeedsShaping(item); |
| } |
| |
| String NGInlineNode::ToString() const { |
| return "NGInlineNode"; |
| } |
| |
| } // namespace blink |