diff options
Diffstat (limited to 'chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc')
-rw-r--r-- | chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc | 608 |
1 files changed, 457 insertions, 151 deletions
diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc index fe15c52f456..6a7e4d86f9d 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc @@ -5,6 +5,7 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.h" #include "base/containers/adapters.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_inline_break_token.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h" @@ -20,7 +21,9 @@ #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/resolved_text_layout_attributes_iterator.h" #include "third_party/blink/renderer/core/style/computed_style.h" +#include "third_party/blink/renderer/core/svg/svg_text_content_element.h" #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_view.h" #include "third_party/blink/renderer/platform/fonts/shaping/shaping_line_breaker.h" #include "third_party/blink/renderer/platform/text/character.h" @@ -157,6 +160,24 @@ void CollectCharIndex(void* context, index_list->push_back(char_index); } +float ComputeWordWidth(const ShapeResult& shape_result, + wtf_size_t start_offset, + wtf_size_t end_offset) { + const wtf_size_t offset_adjust = shape_result.StartIndex(); + const float start_position = + shape_result.CachedPositionForOffset(start_offset - offset_adjust); + const float end_position = + shape_result.CachedPositionForOffset(end_offset - offset_adjust); + return IsLtr(shape_result.Direction()) ? end_position - start_position + : start_position - end_position; +} + +inline LayoutNGTextCombine* MayBeTextCombine(const NGInlineItem* item) { + if (!item) + return nullptr; + return DynamicTo<LayoutNGTextCombine>(item->GetLayoutObject()); +} + } // namespace inline void NGLineBreaker::ClearNeedsLayout(const NGInlineItem& item) { @@ -167,6 +188,16 @@ inline void NGLineBreaker::ClearNeedsLayout(const NGInlineItem& item) { layout_object->ClearNeedsLayout(); } +inline bool NGLineBreaker::ShouldAutoWrap(const ComputedStyle& style) const { + // TODO(crbug.com/366553): SVG <text> should not be auto_wrap_ for now. + if (UNLIKELY(is_svg_text_)) + return false; + // Combine text should not cause line break. + if (UNLIKELY(is_text_combine_)) + return false; + return style.AutoWrap(); +} + LayoutUnit NGLineBreaker::ComputeAvailableWidth() const { LayoutUnit available_width = line_opportunity_.AvailableInlineSize(); // Make sure it's at least the initial size, which is usually 0 but not so @@ -190,7 +221,8 @@ NGLineBreaker::NGLineBreaker(NGInlineNode node, : line_opportunity_(line_opportunity), node_(node), mode_(mode), - is_svg_text_(node.IsSVGText()), + is_svg_text_(node.IsSvgText()), + is_text_combine_(node.IsTextCombine()), is_first_formatted_line_((!break_token || (!break_token->ItemIndex() && !break_token->TextOffset())) && node.CanContainFirstFormattedLine()), @@ -208,12 +240,17 @@ NGLineBreaker::NGLineBreaker(NGInlineNode node, break_token_(break_token), break_iterator_(text_content_), shaper_(text_content_), - spacing_(text_content_), + spacing_(text_content_, is_svg_text_), leading_floats_(leading_floats), handled_leading_floats_index_(handled_leading_floats_index), base_direction_(node_.BaseDirection()) { available_width_ = ComputeAvailableWidth(); break_iterator_.SetBreakSpace(BreakSpaceType::kAfterSpaceRun); + if (is_svg_text_) { + svg_resolved_iterator_ = + std::make_unique<ResolvedTextLayoutAttributesIterator>( + node_.SvgCharacterDataList()); + } if (!break_token) return; @@ -252,6 +289,8 @@ inline NGInlineItemResult* NGLineBreaker::AddItem(const NGInlineItem& item, DCHECK_GE(offset_, item.StartOffset()); DCHECK_GE(end_offset, offset_); DCHECK_LE(end_offset, item.EndOffset()); + if (UNLIKELY(item.IsTextCombine())) + line_info->SetHaveTextCombineItem(); NGInlineItemResults* item_results = line_info->MutableResults(); return &item_results->emplace_back( &item, item_index_, NGTextOffset(offset_, end_offset), @@ -330,7 +369,8 @@ void NGLineBreaker::RecalcClonedBoxDecorations() { has_cloned_box_decorations_ = true; ++cloned_box_decorations_count_; NGInlineItemResult item_result; - ComputeOpenTagResult(*item, constraint_space_, &item_result); + if (!is_svg_text_) + ComputeOpenTagResult(*item, constraint_space_, &item_result); cloned_box_decorations_initial_size_ += item_result.inline_size; cloned_box_decorations_end_size_ += item_result.margins.inline_end + item_result.borders.inline_end + @@ -351,8 +391,7 @@ void NGLineBreaker::RecalcClonedBoxDecorations() { // |position_| LayoutUnit NGLineBreaker::AddHyphen(NGInlineItemResults* item_results, wtf_size_t index, - NGInlineItemResult* item_result, - const NGInlineItem& item) { + NGInlineItemResult* item_result) { DCHECK(!HasHyphen()); DCHECK_EQ(index, static_cast<wtf_size_t>(item_result - item_results->begin())); @@ -360,14 +399,7 @@ LayoutUnit NGLineBreaker::AddHyphen(NGInlineItemResults* item_results, hyphen_index_ = index; if (!item_result->hyphen_string) { - DCHECK(!item_result->hyphen_shape_result); - DCHECK(item.Style()); - const ComputedStyle& style = *item.Style(); - DCHECK(!item_result->hyphen_string); - item_result->hyphen_string = style.HyphenString(); - HarfBuzzShaper shaper(item_result->hyphen_string); - item_result->hyphen_shape_result = - shaper.Shape(&style.GetFont(), style.Direction()); + item_result->ShapeHyphen(); has_any_hyphens_ = true; } DCHECK(item_result->hyphen_string); @@ -383,14 +415,15 @@ LayoutUnit NGLineBreaker::AddHyphen(NGInlineItemResults* item_results, wtf_size_t index) { NGInlineItemResult* item_result = &(*item_results)[index]; DCHECK(item_result->item); - return AddHyphen(item_results, index, item_result, *item_result->item); + return AddHyphen(item_results, index, item_result); } LayoutUnit NGLineBreaker::AddHyphen(NGInlineItemResults* item_results, - NGInlineItemResult* item_result, - const NGInlineItem& item) { - return AddHyphen(item_results, item_result - item_results->begin(), - item_result, item); + NGInlineItemResult* item_result) { + return AddHyphen( + item_results, + base::checked_cast<wtf_size_t>(item_result - item_results->begin()), + item_result); } // Remove the hyphen string from the |NGInlineItemResult|. @@ -416,11 +449,11 @@ void NGLineBreaker::RestoreLastHyphen(NGInlineItemResults* item_results) { DCHECK(has_any_hyphens_); for (NGInlineItemResult& item_result : base::Reversed(*item_results)) { DCHECK(item_result.item); - const NGInlineItem& item = *item_result.item; if (item_result.hyphen_string) { - AddHyphen(item_results, &item_result, item); + AddHyphen(item_results, &item_result); return; } + const NGInlineItem& item = *item_result.item; if (item.Type() == NGInlineItem::kText || item.Type() == NGInlineItem::kAtomicInline) return; @@ -590,6 +623,10 @@ void NGLineBreaker::BreakLine( HandleBidiControlItem(item, line_info); continue; } + if (item.Type() == NGInlineItem::kBlockInInline) { + HandleBlockInInline(item, line_info); + continue; + } // Items after this point are not trailable. If we're trailing, break before // any non-trailable items @@ -638,32 +675,137 @@ void NGLineBreaker::ComputeLineLocation(NGLineInfo* line_info) const { } // Atomic inlines have break opportunities before and after, even when the -// adjacent character is U+00A0 NO-BREAK SPACE character. -bool NGLineBreaker::ShouldForceCanBreakAfter( - const NGInlineItemResult& item_result) const { +// adjacent character is U+00A0 NO-BREAK SPACE character, except when sticky +// images quirk is applied. +// Note: We treat text combine as text content instead of atomic inline box[1]. +// [1] https://drafts.csswg.org/css-writing-modes-3/#text-combine-layout +bool NGLineBreaker::CanBreakAfterAtomicInline(const NGInlineItem& item) const { + DCHECK_EQ(item.Type(), NGInlineItem::kAtomicInline); + if (!auto_wrap_ || item.EndOffset() == Text().length()) + return false; + // We can not break before sticky images quirk was applied. + if (item.IsImage()) + return !sticky_images_quirk_; + + if (item.IsRubyRun()) + return break_iterator_.IsBreakable(item.EndOffset()); + + // Handles text combine + // See "fast/writing-mode/text-combine-line-break.html". + auto* const text_combine = MayBeTextCombine(&item); + if (LIKELY(!text_combine)) + return true; + + // Populate |text_content| with |item| and text content after |item|. + StringBuilder text_content; + NGInlineNode(text_combine).PrepareLayoutIfNeeded(); + text_content.Append(text_combine->GetTextContent()); + const auto text_combine_end_offset = text_content.length(); + auto* const atomic_inline_item = TryGetAtomicInlineItemAfter(item); + if (auto* next_text_combine = MayBeTextCombine(atomic_inline_item)) { + // Note: In |NGLineBreakerMode::k{Min,Max}Content|, we've not laid + // out atomic line box yet. + NGInlineNode(next_text_combine).PrepareLayoutIfNeeded(); + text_content.Append(next_text_combine->GetTextContent()); + } else { + text_content.Append(StringView(Text(), item.EndOffset(), + Text().length() - item.EndOffset())); + } + + DCHECK_EQ(Text(), break_iterator_.GetString()); + LazyLineBreakIterator break_iterator(text_content.ToString(), + break_iterator_.Locale(), + break_iterator_.BreakType()); + break_iterator.SetBreakSpace(break_iterator_.BreakSpace()); + return break_iterator.IsBreakable(text_combine_end_offset); +} + +bool NGLineBreaker::CanBreakAfter(const NGInlineItem& item) const { + DCHECK_NE(item.Type(), NGInlineItem::kAtomicInline); + DCHECK(auto_wrap_); + const bool can_break_after = break_iterator_.IsBreakable(item.EndOffset()); + if (item.Type() != NGInlineItem::kText) { + DCHECK_EQ(item.Type(), NGInlineItem::kControl) << "We get the test case!"; + // Example: <div>12345\t\t678</div> + // NGInlineItem[0] kText "12345" + // NGInlineItem[1] kControl "\t\t" + // NGInlineItem[2] kText "678" + // See NGLineBreakerTest.OverflowTab + return can_break_after; + } + auto* const atomic_inline_item = TryGetAtomicInlineItemAfter(item); + if (!atomic_inline_item) + return can_break_after; + + if (atomic_inline_item->IsRubyRun()) + return can_break_after; + + // We can not break before sticky images quirk was applied. + if (UNLIKELY(Text()[atomic_inline_item->StartOffset()] == + kNoBreakSpaceCharacter)) { + // "One " <img> => We can break after "One ". + // "One" <img> => We can not break after "One". + // See "tables/mozilla/bugs/bug101674.html" + DCHECK(atomic_inline_item->IsImage() && sticky_images_quirk_); + return can_break_after; + } + + // Handles text combine as its text contents followed by |item|. + // See "fast/writing-mode/text-combine-line-break.html". + auto* const text_combine = MayBeTextCombine(atomic_inline_item); + if (LIKELY(!text_combine)) + return true; + + // Populate |text_content| with |item| and |text_combine|. + // Following test reach here: + // * fast/writing-mode/text-combine-compress.html + // * virtual/text-antialias/international/text-combine-image-test.html + // * virtual/text-antialias/international/text-combine-text-transform.html + StringBuilder text_content; + text_content.Append(StringView(Text(), item.StartOffset(), item.Length())); + const auto item_end_offset = text_content.length(); + // Note: In |NGLineBreakerMode::k{Min,Max}Content|, we've not laid out + // atomic line box yet. + NGInlineNode(text_combine).PrepareLayoutIfNeeded(); + text_content.Append(text_combine->GetTextContent()); + + DCHECK_EQ(Text(), break_iterator_.GetString()); + LazyLineBreakIterator break_iterator(text_content.ToString(), + break_iterator_.Locale(), + break_iterator_.BreakType()); + break_iterator.SetBreakSpace(break_iterator_.BreakSpace()); + return break_iterator.IsBreakable(item_end_offset); +} + +bool NGLineBreaker::MayBeAtomicInline(wtf_size_t offset) const { + DCHECK_LT(offset, Text().length()); + const auto char_code = Text()[offset]; + if (char_code == kObjectReplacementCharacter) + return true; + return sticky_images_quirk_ && char_code == kNoBreakSpaceCharacter; +} + +const NGInlineItem* NGLineBreaker::TryGetAtomicInlineItemAfter( + const NGInlineItem& item) const { DCHECK(auto_wrap_); - DCHECK_EQ(item_result.item->Type(), NGInlineItem::kText); const String& text = Text(); - DCHECK_GE(text.length(), item_result.EndOffset()); - if (text.length() <= item_result.EndOffset() || - text[item_result.EndOffset()] != kObjectReplacementCharacter) - return false; + if (item.EndOffset() == text.length()) + return nullptr; + if (!MayBeAtomicInline(item.EndOffset())) + return nullptr; + // This kObjectReplacementCharacter can be any objects, such as a floating or // an OOF object. Check if it's really an atomic inline. const Vector<NGInlineItem>& items = Items(); - for (const NGInlineItem* item = std::next(item_result.item); - item != items.end(); ++item) { - DCHECK_EQ(item->StartOffset(), item_result.EndOffset()); - if (item->Type() == NGInlineItem::kAtomicInline) { - // Except when sticky images quirk was applied. - if (UNLIKELY(text[item->StartOffset()] == kNoBreakSpaceCharacter)) - return false; - return !item->IsRubyRun(); - } - if (item->EndOffset() > item_result.EndOffset()) - break; + for (const NGInlineItem* next_item = std::next(&item); + next_item != items.end(); ++next_item) { + DCHECK_EQ(next_item->StartOffset(), item.EndOffset()); + if (next_item->Type() == NGInlineItem::kAtomicInline) + return next_item; + if (next_item->EndOffset() > item.EndOffset()) + return nullptr; } - return false; + return nullptr; } void NGLineBreaker::HandleText(const NGInlineItem& item, @@ -673,7 +815,7 @@ void NGLineBreaker::HandleText(const NGInlineItem& item, (item.Type() == NGInlineItem::kControl && Text()[item.StartOffset()] == kTabulationCharacter)); DCHECK(&shape_result); - DCHECK_EQ(auto_wrap_, !is_svg_text_ && item.Style()->AutoWrap()); + DCHECK_EQ(auto_wrap_, ShouldAutoWrap(*item.Style())); // If we're trailing, only trailing spaces can be included in this line. if (UNLIKELY(state_ == LineBreakState::kTrailing)) { @@ -781,7 +923,8 @@ void NGLineBreaker::HandleText(const NGInlineItem& item, state_ = LineBreakState::kTrailing; if (item_result->item->Style()->WhiteSpace() == EWhiteSpace::kPreWrap && IsBreakableSpace(Text()[item_result->EndOffset() - 1])) { - unsigned end_index = item_result - line_info->Results().begin(); + unsigned end_index = base::checked_cast<unsigned>( + item_result - line_info->Results().begin()); Rewind(end_index, line_info); } return; @@ -805,7 +948,7 @@ void NGLineBreaker::HandleText(const NGInlineItem& item, } if (is_svg_text_) { - SplitTextByGlyphs(item, line_info); + SplitTextIntoSegements(item, line_info); return; } @@ -834,15 +977,14 @@ void NGLineBreaker::HandleText(const NGInlineItem& item, MoveToNextOf(item); } -// In SVG <text>, we produce NGInlineItemResult split by glyphs for simplicity. +// In SVG <text>, we produce NGInlineItemResult split into segments partitioned +// by x/y/dx/dy/rotate attributes. +// // Split in PrepareLayout() or after producing NGFragmentItem would need // additional memory overhead. So we split in NGLineBreaker while it converts // NGInlineItems to NGInlineItemResults. -// -// TODO(crbug.com/1179585): Ideally we should split the text by segments -// partitioned by x/y/dx/dy/rotate attributes. -void NGLineBreaker::SplitTextByGlyphs(const NGInlineItem& item, - NGLineInfo* line_info) { +void NGLineBreaker::SplitTextIntoSegements(const NGInlineItem& item, + NGLineInfo* line_info) { DCHECK(RuntimeEnabledFeatures::SVGTextNGEnabled()); DCHECK(is_svg_text_); DCHECK_EQ(offset_, item.StartOffset()); @@ -854,15 +996,35 @@ void NGLineBreaker::SplitTextByGlyphs(const NGInlineItem& item, if (shape.IsRtl()) index_list.Reverse(); wtf_size_t size = index_list.size(); + unsigned glyph_start = offset_; for (wtf_size_t i = 0; i < size; ++i) { - DCHECK_EQ(offset_, index_list[i]); + DCHECK_EQ(glyph_start, index_list[i]); unsigned glyph_end = i + 1 < size ? index_list[i + 1] : shape.EndIndex(); + StringView text_view(Text()); + bool should_split = i == size - 1; + for (; glyph_start < glyph_end; + glyph_start = text_view.NextCodePointOffset(glyph_start)) { + ++svg_addressable_offset_; + should_split = should_split || ShouldCreateNewSvgSegment(); + } + if (!should_split) + continue; NGInlineItemResult* result = AddItem(item, glyph_end, line_info); result->should_create_line_box = true; auto shape_result_view = ShapeResultView::Create(&shape, offset_, glyph_end); - result->inline_size = - shape_result_view->SnappedWidth().ClampNegativeToZero(); + // For general CSS text, we apply SnappedWidth().ClampNegativeToZero(). + // However we need to remove ClampNegativeToZero() for SVG <text> in order + // to get similar character positioning. + // + // For general CSS text, a negative word-spacing value decreases + // inline_size of an NGInlineItemResult consisting of multiple characters, + // and the inline_size rarely becomes negative. However, for SVG <text>, + // it decreases inline_size of an NGInlineItemResult consisting of only a + // space character, and the inline_size becomes negative easily. + // + // See svg/W3C-SVG-1.1/text-spacing-01-b.svg. + result->inline_size = shape_result_view->SnappedWidth(); result->shape_result = std::move(shape_result_view); offset_ = glyph_end; position_ += result->inline_size; @@ -871,6 +1033,28 @@ void NGLineBreaker::SplitTextByGlyphs(const NGInlineItem& item, MoveToNextOf(item); } +bool NGLineBreaker::ShouldCreateNewSvgSegment() const { + DCHECK(is_svg_text_); + for (const auto& range : node_.SvgTextPathRangeList()) { + if (range.start_index <= svg_addressable_offset_ && + svg_addressable_offset_ <= range.end_index) + return true; + } + for (const auto& range : node_.SvgTextLengthRangeList()) { + if (To<SVGTextContentElement>(range.layout_object->GetNode()) + ->lengthAdjust() + ->CurrentEnumValue() == kSVGLengthAdjustSpacingAndGlyphs) + continue; + if (range.start_index <= svg_addressable_offset_ && + svg_addressable_offset_ <= range.end_index) + return true; + } + const NGSvgCharacterData& char_data = + svg_resolved_iterator_->AdvanceTo(svg_addressable_offset_); + return char_data.HasRotate() || char_data.HasX() || char_data.HasY() || + char_data.HasDx() || char_data.HasDy(); +} + NGLineBreaker::BreakResult NGLineBreaker::BreakText( NGInlineItemResult* item_result, const NGInlineItem& item, @@ -960,7 +1144,7 @@ NGLineBreaker::BreakResult NGLineBreaker::BreakText( if (UNLIKELY(result.is_hyphenated)) { NGInlineItemResults* item_results = line_info->MutableResults(); const LayoutUnit hyphen_inline_size = - AddHyphen(item_results, item_result, item); + AddHyphen(item_results, item_result); // If the hyphen overflows, retry with the reduced available width. if (!result.is_overflow && inline_size <= available_width) { const LayoutUnit space_for_hyphen = @@ -1000,11 +1184,7 @@ NGLineBreaker::BreakResult NGLineBreaker::BreakText( } } else { DCHECK_EQ(item_result->EndOffset(), item.EndOffset()); - item_result->can_break_after = - break_iterator_.IsBreakable(item_result->EndOffset()); - if (!item_result->can_break_after && item.Type() == NGInlineItem::kText && - ShouldForceCanBreakAfter(*item_result)) - item_result->can_break_after = true; + item_result->can_break_after = CanBreakAfter(item); trailing_whitespace_ = WhitespaceState::kUnknown; } @@ -1027,6 +1207,7 @@ bool NGLineBreaker::BreakTextAtPreviousBreakOpportunity( const NGInlineItem& item = *item_result->item; DCHECK_EQ(item.Type(), NGInlineItem::kText); DCHECK(item.Style() && item.Style()->AutoWrap()); + DCHECK(!is_text_combine_); // TODO(jfernandez): Should we use the non-hangable-run-end instead ? unsigned break_opportunity = break_iterator_.PreviousBreakOpportunity( @@ -1085,10 +1266,6 @@ bool NGLineBreaker::HandleTextForFastMinContent(NGInlineItemResult* item_result, if (fast_min_content_item_ == &item) return false; - // Hyphenation is not supported yet. - if (hyphenation_) - return false; - absl::optional<LineBreakType> saved_line_break_type; if (break_anywhere_if_overflow_ && !override_break_anywhere_) { saved_line_break_type = break_iterator_.BreakType(); @@ -1102,9 +1279,11 @@ bool NGLineBreaker::HandleTextForFastMinContent(NGInlineItemResult* item_result, const String& text = Text(); float min_width = 0; unsigned last_end_offset = 0; + unsigned end_offset = start_offset + 1; + absl::optional<LayoutUnit> hyphen_inline_size; while (start_offset < item.EndOffset()) { - unsigned end_offset = break_iterator_.NextBreakOpportunity( - start_offset + 1, item.EndOffset()); + end_offset = + break_iterator_.NextBreakOpportunity(end_offset, item.EndOffset()); unsigned non_hangable_run_end = end_offset; if (item.Style()->WhiteSpace() != EWhiteSpace::kBreakSpaces) { @@ -1118,19 +1297,60 @@ bool NGLineBreaker::HandleTextForFastMinContent(NGInlineItemResult* item_result, if (end_offset >= item.EndOffset()) break; - // Inserting a hyphenation character is not supported yet. - // TODO (jfernandez): Maybe we need to use 'end_offset' here ? - if (text[non_hangable_run_end - 1] == kSoftHyphenCharacter) - return false; + // Ignore soft-hyphen opportunities if `hyphens: none`. + bool has_hyphen = text[non_hangable_run_end - 1] == kSoftHyphenCharacter; + if (has_hyphen && !enable_soft_hyphen_) { + ++end_offset; + if (end_offset < item.EndOffset()) + continue; + has_hyphen = false; + } + + if (UNLIKELY(hyphenation_)) { + // When 'hyphens: auto', compute all hyphenation opportunities. + if (!hyphen_inline_size) { + if (!item_result->hyphen_shape_result) + item_result->ShapeHyphen(); + hyphen_inline_size = item_result->HyphenInlineSize(); + } + wtf_size_t word_len = non_hangable_run_end - start_offset; + const StringView word(text, start_offset, word_len); + Vector<wtf_size_t, 8> locations = hyphenation_->HyphenLocations(word); + // |locations| is a list of hyphenation points in the descending order. + // Append 0 to process all parts the same way. + DCHECK(std::is_sorted(locations.rbegin(), locations.rend())); + DCHECK(!locations.Contains(0u)); + DCHECK(!locations.Contains(word_len)); + locations.push_back(0); + LayoutUnit max_part_width; + for (const wtf_size_t location : locations) { + LayoutUnit part_width = LayoutUnit::FromFloatCeil(ComputeWordWidth( + shape_result, start_offset + location, start_offset + word_len)); + if (has_hyphen) + part_width += *hyphen_inline_size; + max_part_width = std::max(part_width, max_part_width); + word_len = location; + has_hyphen = true; + } + min_width = std::max(max_part_width.ToFloat(), min_width); + } else { + float word_width = + ComputeWordWidth(shape_result, start_offset, non_hangable_run_end); + + // Append hyphen-width to `word_width` if the word is hyphenated. + if (has_hyphen) { + if (!hyphen_inline_size) { + if (!item_result->hyphen_shape_result) + item_result->ShapeHyphen(); + hyphen_inline_size = item_result->HyphenInlineSize(); + } + word_width = + (LayoutUnit::FromFloatCeil(word_width) + *hyphen_inline_size) + .ToFloat(); + } - float start_position = shape_result.CachedPositionForOffset( - start_offset - shape_result.StartIndex()); - float end_position = shape_result.CachedPositionForOffset( - non_hangable_run_end - shape_result.StartIndex()); - float word_width = IsLtr(shape_result.Direction()) - ? end_position - start_position - : start_position - end_position; - min_width = std::max(word_width, min_width); + min_width = std::max(word_width, min_width); + } last_end_offset = non_hangable_run_end; // TODO (jfernandez): I think that having the non_hangable_run_end @@ -1140,6 +1360,7 @@ bool NGLineBreaker::HandleTextForFastMinContent(NGInlineItemResult* item_result, IsBreakableSpace(text[start_offset])) { ++start_offset; } + end_offset = start_offset + 1; } if (saved_line_break_type.has_value()) @@ -1186,7 +1407,7 @@ scoped_refptr<ShapeResult> NGLineBreaker::ShapeText(const NGInlineItem& item, } else { shape_result = items_data_.segments->ShapeText( &shaper_, &item.Style()->GetFont(), item.Direction(), start, end, - &item - items_data_.items.begin()); + base::checked_cast<unsigned>(&item - items_data_.items.begin())); } if (UNLIKELY(spacing_.HasSpacing())) shape_result->ApplySpacing(spacing_); @@ -1268,6 +1489,7 @@ void NGLineBreaker::HandleTrailingSpaces(const NGInlineItem& item, state_ = LineBreakState::kDone; return; } + DCHECK(!is_text_combine_); if (style.CollapseWhiteSpace() && !Character::IsOtherSpaceSeparator(text[offset_])) { @@ -1344,9 +1566,7 @@ void NGLineBreaker::HandleTrailingSpaces(const NGInlineItem& item, state_ = LineBreakState::kTrailing; } -// Remove trailing collapsible spaces in |line_info|. -// https://drafts.csswg.org/css-text-3/#white-space-phase-2 -void NGLineBreaker::RemoveTrailingCollapsibleSpace(NGLineInfo* line_info) { +void NGLineBreaker::RewindTrailingOpenTags(NGLineInfo* line_info) { // Remove trailing open tags. Open tags are included as trailable items // because they are ambiguous. When the line ends, and if the end of line has // open tags, they are not trailable. @@ -1357,7 +1577,8 @@ void NGLineBreaker::RemoveTrailingCollapsibleSpace(NGLineInfo* line_info) { for (const NGInlineItemResult& item_result : base::Reversed(item_results)) { DCHECK(item_result.item); if (item_result.item->Type() != NGInlineItem::kOpenTag) { - unsigned end_index = &item_result - item_results.begin() + 1; + unsigned end_index = + base::checked_cast<unsigned>(&item_result - item_results.begin() + 1); if (end_index < item_results.size()) { const NGInlineItemResult& end_item_result = item_results[end_index]; unsigned end_item_index = end_item_result.item_index; @@ -1371,6 +1592,15 @@ void NGLineBreaker::RemoveTrailingCollapsibleSpace(NGLineInfo* line_info) { break; } } +} + +// Remove trailing collapsible spaces in |line_info|. +// https://drafts.csswg.org/css-text-3/#white-space-phase-2 +void NGLineBreaker::RemoveTrailingCollapsibleSpace(NGLineInfo* line_info) { + // Rewind trailing open-tags to wrap before them, except when this line ends + // with a forced break, including the one implied by block-in-inline. + if (!is_after_forced_break_) + RewindTrailingOpenTags(line_info); ComputeTrailingCollapsibleSpace(line_info); if (!trailing_collapsible_space_.has_value()) { @@ -1426,9 +1656,7 @@ void NGLineBreaker::ComputeTrailingCollapsibleSpace(NGLineInfo* line_info) { trailing_whitespace_ = WhitespaceState::kNone; const String& text = Text(); - NGInlineItemResults* item_results = line_info->MutableResults(); - for (auto it = item_results->rbegin(); it != item_results->rend(); ++it) { - NGInlineItemResult& item_result = *it; + for (auto& item_result : base::Reversed(*line_info->MutableResults())) { DCHECK(item_result.item); const NGInlineItem& item = *item_result.item; if (item.EndCollapseType() == NGInlineItem::kOpaqueToCollapsing) @@ -1475,22 +1703,21 @@ void NGLineBreaker::ComputeTrailingCollapsibleSpace(NGLineInfo* line_info) { trailing_collapsible_space_.reset(); } -// Measure control items; new lines and tab, that are similar to text, affect -// layout, but do not need shaping/painting. -void NGLineBreaker::HandleControlItem(const NGInlineItem& item, - NGLineInfo* line_info) { - DCHECK_GE(item.Length(), 1u); - if (item.TextType() == NGTextType::kForcedLineBreak) { - DCHECK_EQ(Text()[item.StartOffset()], kNewlineCharacter); +// |item| is |nullptr| if this is an implicit forced break. +void NGLineBreaker::HandleForcedLineBreak(const NGInlineItem* item, + NGLineInfo* line_info) { + // Check overflow, because the last item may have overflowed. + if (HandleOverflowIfNeeded(line_info)) + return; - // Check overflow, because the last item may have overflowed. - if (HandleOverflowIfNeeded(line_info)) - return; + if (item) { + DCHECK_EQ(item->TextType(), NGTextType::kForcedLineBreak); + DCHECK_EQ(Text()[item->StartOffset()], kNewlineCharacter); - NGInlineItemResult* item_result = AddItem(item, line_info); + NGInlineItemResult* item_result = AddItem(*item, line_info); item_result->should_create_line_box = true; item_result->has_only_trailing_spaces = true; - MoveToNextOf(item); + MoveToNextOf(*item); // Include following close tags. The difference is visible when they have // margin/border/padding. @@ -1511,12 +1738,23 @@ void NGLineBreaker::HandleControlItem(const NGInlineItem& item, } break; } + } - if (UNLIKELY(HasHyphen())) - position_ -= RemoveHyphen(line_info->MutableResults()); - is_after_forced_break_ = true; - line_info->SetIsLastLine(true); - state_ = LineBreakState::kDone; + if (UNLIKELY(HasHyphen())) + position_ -= RemoveHyphen(line_info->MutableResults()); + is_after_forced_break_ = true; + line_info->SetHasForcedBreak(); + line_info->SetIsLastLine(true); + state_ = LineBreakState::kDone; +} + +// Measure control items; new lines and tab, that are similar to text, affect +// layout, but do not need shaping/painting. +void NGLineBreaker::HandleControlItem(const NGInlineItem& item, + NGLineInfo* line_info) { + DCHECK_GE(item.Length(), 1u); + if (item.TextType() == NGTextType::kForcedLineBreak) { + HandleForcedLineBreak(&item, line_info); return; } @@ -1666,54 +1904,18 @@ void NGLineBreaker::HandleAtomicInline( item_result->layout_result->PhysicalFragment()) .InlineSize(); item_result->inline_size += inline_margins; - } else if (mode_ == NGLineBreakerMode::kMaxContent && max_size_cache_) { - unsigned item_index = &item - Items().begin(); - item_result->inline_size = (*max_size_cache_)[item_index]; } else { - DCHECK(mode_ == NGLineBreakerMode::kMinContent || !max_size_cache_); - NGBlockNode child(To<LayoutBox>(item.GetLayoutObject())); - - NGMinMaxConstraintSpaceBuilder builder(constraint_space_, node_.Style(), - child, /* is_new_fc */ true); - builder.SetAvailableBlockSize(constraint_space_.AvailableSize().block_size); - builder.SetPercentageResolutionBlockSize( - constraint_space_.PercentageResolutionBlockSize()); - builder.SetReplacedPercentageResolutionBlockSize( - constraint_space_.ReplacedPercentageResolutionBlockSize()); - const auto space = builder.ToConstraintSpace(); - - MinMaxSizesResult result = - ComputeMinAndMaxContentContribution(node_.Style(), child, space); - if (mode_ == NGLineBreakerMode::kMinContent) { - item_result->inline_size = result.sizes.min_size + inline_margins; - if (depends_on_block_constraints_out_) { - *depends_on_block_constraints_out_ |= - result.depends_on_block_constraints; - } - if (max_size_cache_) { - if (max_size_cache_->IsEmpty()) - max_size_cache_->resize(Items().size()); - unsigned item_index = &item - Items().begin(); - (*max_size_cache_)[item_index] = result.sizes.max_size + inline_margins; - } - } else { - item_result->inline_size = result.sizes.max_size + inline_margins; - } + DCHECK(mode_ == NGLineBreakerMode::kMaxContent || + mode_ == NGLineBreakerMode::kMinContent); + ComputeMinMaxContentSizeForBlockChild(item, item_result); } item_result->should_create_line_box = true; - // Atomic inlines have break opportunities before and after, even when the - // adjacent character is U+00A0 NO-BREAK SPACE character, except when sticky - // images quirk is applied. - item_result->can_break_after = - auto_wrap_ && !(sticky_images_quirk_ && item.IsImage()); + item_result->can_break_after = CanBreakAfterAtomicInline(item); position_ += item_result->inline_size; if (item.IsRubyRun()) { - // Overrides can_break_after. - ComputeCanBreakAfter(item_result, auto_wrap_, break_iterator_); - NGAnnotationOverhang overhang = GetOverhang(*item_result); if (overhang.end > LayoutUnit()) { item_result->pending_end_overhang = overhang.end; @@ -1732,6 +1934,100 @@ void NGLineBreaker::HandleAtomicInline( MoveToNextOf(item); } +void NGLineBreaker::ComputeMinMaxContentSizeForBlockChild( + const NGInlineItem& item, + NGInlineItemResult* item_result) { + DCHECK(mode_ == NGLineBreakerMode::kMaxContent || + mode_ == NGLineBreakerMode::kMinContent); + if (mode_ == NGLineBreakerMode::kMaxContent && max_size_cache_) { + const unsigned item_index = + base::checked_cast<unsigned>(&item - Items().begin()); + item_result->inline_size = (*max_size_cache_)[item_index]; + return; + } + + DCHECK(mode_ == NGLineBreakerMode::kMinContent || !max_size_cache_); + NGBlockNode child(To<LayoutBox>(item.GetLayoutObject())); + + NGMinMaxConstraintSpaceBuilder builder(constraint_space_, node_.Style(), + child, /* is_new_fc */ true); + builder.SetAvailableBlockSize(constraint_space_.AvailableSize().block_size); + builder.SetPercentageResolutionBlockSize( + constraint_space_.PercentageResolutionBlockSize()); + builder.SetReplacedPercentageResolutionBlockSize( + constraint_space_.ReplacedPercentageResolutionBlockSize()); + const auto space = builder.ToConstraintSpace(); + + const MinMaxSizesResult result = + ComputeMinAndMaxContentContribution(node_.Style(), child, space); + const LayoutUnit inline_margins = item_result->margins.InlineSum(); + if (mode_ == NGLineBreakerMode::kMinContent) { + item_result->inline_size = result.sizes.min_size + inline_margins; + if (depends_on_block_constraints_out_) + *depends_on_block_constraints_out_ |= result.depends_on_block_constraints; + if (max_size_cache_) { + if (max_size_cache_->IsEmpty()) + max_size_cache_->resize(Items().size()); + const unsigned item_index = + base::checked_cast<unsigned>(&item - Items().begin()); + (*max_size_cache_)[item_index] = result.sizes.max_size + inline_margins; + } + return; + } + + DCHECK(mode_ == NGLineBreakerMode::kMaxContent && !max_size_cache_); + item_result->inline_size = result.sizes.max_size + inline_margins; +} + +void NGLineBreaker::HandleBlockInInline(const NGInlineItem& item, + NGLineInfo* line_info) { + DCHECK_EQ(item.Type(), NGInlineItem::kBlockInInline); + + if (!line_info->Results().IsEmpty()) { + // If there were any items, force a line break before this item. + HandleForcedLineBreak(nullptr, line_info); + return; + } + + NGInlineItemResult* item_result = AddItem(item, line_info); + if (mode_ == NGLineBreakerMode::kContent) { + const NGBlockBreakToken* block_break_token = + break_token_ ? break_token_->BlockInInlineBreakToken() : nullptr; + scoped_refptr<const NGLayoutResult> layout_result = + NGBlockNode(To<LayoutBox>(item.GetLayoutObject())) + .Layout(constraint_space_, block_break_token); + if (layout_result->Status() != NGLayoutResult::kSuccess) { + line_info->SetAbortedLayoutResult(std::move(layout_result)); + state_ = LineBreakState::kDone; + return; + } + const NGPhysicalFragment& fragment = layout_result->PhysicalFragment(); + item_result->inline_size = + NGFragment(constraint_space_.GetWritingDirection(), fragment) + .InlineSize(); + + item_result->should_create_line_box = !layout_result->IsSelfCollapsing(); + item_result->layout_result = std::move(layout_result); + + DCHECK(!line_info->BlockInInlineBreakToken()); + line_info->SetBlockInInlineBreakToken( + To<NGBlockBreakToken>(fragment.BreakToken())); + } else { + DCHECK(mode_ == NGLineBreakerMode::kMaxContent || + mode_ == NGLineBreakerMode::kMinContent); + ComputeMinMaxContentSizeForBlockChild(item, item_result); + } + + position_ += item_result->inline_size; + line_info->SetIsBlockInInline(); + line_info->SetHasForcedBreak(); + is_after_forced_break_ = true; + trailing_whitespace_ = WhitespaceState::kNone; + if (!line_info->BlockInInlineBreakToken()) + MoveToNextOf(item); + state_ = LineBreakState::kDone; +} + // Performs layout and positions a float. // // If there is a known available_width (e.g. something has resolved the @@ -1907,7 +2203,8 @@ void NGLineBreaker::HandleOpenTag(const NGInlineItem& item, NGInlineItemResult* item_result = AddItem(item, line_info); DCHECK(item.Style()); const ComputedStyle& style = *item.Style(); - if (ComputeOpenTagResult(item, constraint_space_, item_result)) { + if (!is_svg_text_ && + ComputeOpenTagResult(item, constraint_space_, item_result)) { // Negative margins on open tags may bring the position back. Update // |state_| if that happens. if (UNLIKELY(item_result->inline_size < 0 && @@ -1955,7 +2252,7 @@ void NGLineBreaker::HandleCloseTag(const NGInlineItem& item, NGInlineItemResult* item_result = AddItem(item, line_info); item_result->has_edge = item.HasEndEdge(); - if (item_result->has_edge) { + if (item_result->has_edge && !is_svg_text_) { DCHECK(item.Style()); const ComputedStyle& style = *item.Style(); item_result->inline_size = ComputeInlineEndSize(constraint_space_, &style); @@ -1982,6 +2279,13 @@ void NGLineBreaker::HandleCloseTag(const NGInlineItem& item, const NGInlineItemResults& item_results = line_info->Results(); if (item_results.size() >= 2) { NGInlineItemResult* last = std::prev(item_result); + if (UNLIKELY(IsA<LayoutNGTextCombine>(last->item->GetLayoutObject()))) { + // |can_break_after| for close tag should be as same as text-combine box. + // See "text-combine-upright-break-inside-001a.html" + // e.g. A<tcy style="white-space: pre">x y</tcy>B + item_result->can_break_after = last->can_break_after; + return; + } if (was_auto_wrap || last->can_break_after) { item_result->can_break_after = last->can_break_after || @@ -2197,7 +2501,7 @@ void NGLineBreaker::RewindOverflow(unsigned new_end, NGLineInfo* line_info) { // controls are trailable. DCHECK_NE(text[item_result.StartOffset()], kNewlineCharacter); DCHECK(item.Style()); - EWhiteSpace white_space = item.Style()->WhiteSpace(); + const EWhiteSpace white_space = item.Style()->WhiteSpace(); if (ComputedStyle::AutoWrap(white_space) && white_space != EWhiteSpace::kBreakSpaces) continue; @@ -2386,13 +2690,13 @@ void NGLineBreaker::SetCurrentStyle(const ComputedStyle& style) { if (&style == current_style_.get()) { #if DCHECK_IS_ON() // Check that cache fields are already setup correctly. - DCHECK_EQ(auto_wrap_, !is_svg_text_ && style.AutoWrap()); + DCHECK_EQ(auto_wrap_, ShouldAutoWrap(style)); if (auto_wrap_) { DCHECK_EQ(enable_soft_hyphen_, style.GetHyphens() != Hyphens::kNone); DCHECK_EQ(break_iterator_.Locale(), style.LocaleForLineBreakIterator()); } - ShapeResultSpacing<String> spacing(spacing_.Text()); - spacing.SetSpacing(style.GetFont()); + ShapeResultSpacing<String> spacing(spacing_.Text(), is_svg_text_); + spacing.SetSpacing(style.GetFont().GetFontDescription()); DCHECK_EQ(spacing.LetterSpacing(), spacing_.LetterSpacing()); DCHECK_EQ(spacing.WordSpacing(), spacing_.WordSpacing()); #endif @@ -2400,10 +2704,10 @@ void NGLineBreaker::SetCurrentStyle(const ComputedStyle& style) { } current_style_ = &style; - // TODO(crbug.com/366553): SVG <text> should not be auto_wrap_ for now. - auto_wrap_ = !is_svg_text_ && style.AutoWrap(); + auto_wrap_ = ShouldAutoWrap(style); if (auto_wrap_) { + DCHECK(!is_text_combine_); LineBreakType line_break_type; EWordBreak word_break = style.WordBreak(); switch (word_break) { @@ -2442,7 +2746,7 @@ void NGLineBreaker::SetCurrentStyle(const ComputedStyle& style) { break_iterator_.SetLocale(style.LocaleForLineBreakIterator()); } - spacing_.SetSpacing(style.GetFont()); + spacing_.SetSpacing(style.GetFont().GetFontDescription()); } bool NGLineBreaker::IsPreviousItemOfType(NGInlineItem::NGInlineItemType type) { @@ -2477,6 +2781,7 @@ scoped_refptr<NGInlineBreakToken> NGLineBreaker::CreateBreakToken( DCHECK_LE(item_index_, items.size()); if (item_index_ >= items.size()) return nullptr; + DCHECK_EQ(line_info.HasForcedBreak(), is_after_forced_break_); return NGInlineBreakToken::Create( node_, current_style_.get(), item_index_, offset_, (is_after_forced_break_ ? NGInlineBreakToken::kIsForcedBreak : 0) | @@ -2485,7 +2790,8 @@ scoped_refptr<NGInlineBreakToken> NGLineBreaker::CreateBreakToken( : 0) | (cloned_box_decorations_count_ ? NGInlineBreakToken::kHasClonedBoxDecorations - : 0)); + : 0), + line_info.BlockInInlineBreakToken()); } void NGLineBreaker::PropagateBreakToken( |