| Index: third_party/WebKit/Source/core/layout/ng/inline/ng_line_breaker.cc
|
| diff --git a/third_party/WebKit/Source/core/layout/ng/inline/ng_line_breaker.cc b/third_party/WebKit/Source/core/layout/ng/inline/ng_line_breaker.cc
|
| index c922def3ee83bb50eab69d404af2a44f84af82a7..7cadf42c80ba54a217a2467542e15aa34e2a4a4a 100644
|
| --- a/third_party/WebKit/Source/core/layout/ng/inline/ng_line_breaker.cc
|
| +++ b/third_party/WebKit/Source/core/layout/ng/inline/ng_line_breaker.cc
|
| @@ -4,6 +4,7 @@
|
|
|
| #include "core/layout/ng/inline/ng_line_breaker.h"
|
|
|
| +#include "core/layout/ng/inline/ng_inline_break_token.h"
|
| #include "core/layout/ng/inline/ng_inline_layout_algorithm.h"
|
| #include "core/layout/ng/inline/ng_inline_node.h"
|
| #include "core/layout/ng/inline/ng_text_fragment.h"
|
| @@ -13,85 +14,312 @@
|
| #include "core/layout/ng/ng_fragment_builder.h"
|
| #include "core/layout/ng/ng_layout_opportunity_iterator.h"
|
| #include "core/style/ComputedStyle.h"
|
| +#include "platform/fonts/shaping/HarfBuzzShaper.h"
|
| +#include "platform/fonts/shaping/ShapingLineBreaker.h"
|
| #include "platform/text/TextBreakIterator.h"
|
|
|
| namespace blink {
|
|
|
| -static bool IsHangable(UChar ch) {
|
| - return ch == ' ';
|
| -}
|
| +namespace {
|
|
|
| -void NGLineBreaker::BreakLines(NGInlineLayoutAlgorithm* algorithm,
|
| - const String& text_content,
|
| - unsigned current_offset) {
|
| - DCHECK(!text_content.IsEmpty());
|
| - LazyLineBreakIterator line_break_iterator(text_content, locale_);
|
| - const unsigned end_offset = text_content.length();
|
| - while (current_offset < end_offset) {
|
| - // Find the next break opportunity.
|
| - int tmp_next_breakable_offset = -1;
|
| - line_break_iterator.IsBreakable(current_offset + 1,
|
| - tmp_next_breakable_offset);
|
| - current_offset =
|
| - tmp_next_breakable_offset >= 0 ? tmp_next_breakable_offset : end_offset;
|
| - DCHECK_LE(current_offset, end_offset);
|
| -
|
| - // Advance the break opportunity to the end of hangable characters; e.g.,
|
| - // spaces.
|
| - // Unlike the ICU line breaker, LazyLineBreakIterator breaks before
|
| - // breakable spaces, and expect the line breaker to handle spaces
|
| - // differently. This logic computes in the ICU way; break after spaces, and
|
| - // handle spaces as hangable characters.
|
| - unsigned start_of_hangables = current_offset;
|
| - while (current_offset < end_offset &&
|
| - IsHangable(text_content[current_offset]))
|
| - current_offset++;
|
| -
|
| - // Set the end to the next break opportunity.
|
| - algorithm->SetEnd(current_offset);
|
| -
|
| - // If there are more available spaces, mark the break opportunity and fetch
|
| - // more text.
|
| - // TODO(layout-ng): check if the height of the linebox can fit within
|
| - // the current opportunity.
|
| - if (algorithm->CanFitOnLine()) {
|
| - algorithm->SetBreakOpportunity();
|
| - continue;
|
| +// Use a mock of ShapingLineBreaker for test/debug purposes.
|
| +#define MOCK_SHAPE_LINE
|
| +
|
| +#if defined(MOCK_SHAPE_LINE)
|
| +// The mock for ShapingLineBreaker::ShapeLine().
|
| +// Given the design of ShapingLineBreaker, expected semantics are:
|
| +// - The returned offset is always > item.StartOffset().
|
| +// - offset < item.EndOffset():
|
| +// - width <= available_width: the break opportunity to fit is found.
|
| +// - width > available_width: the first break opportunity did not fit.
|
| +// - offset == item.EndOffset():
|
| +// - width <= available_width: the break opportunity at the end of the item
|
| +// fits.
|
| +// - width > available_width: the first break opportunity is at the end of
|
| +// the item and it does not fit.
|
| +// - offset > item.EndOffset():, the first break opportunity is beyond the
|
| +// end of item and thus cannot measure. In this case, inline_size shows the
|
| +// width until the end of the item. It may fit or may not.
|
| +std::pair<unsigned, LayoutUnit> ShapeLineMock(
|
| + const NGInlineItem& item,
|
| + unsigned offset,
|
| + LayoutUnit available_width,
|
| + const LazyLineBreakIterator& break_iterator) {
|
| + bool has_break_opportunities = false;
|
| + LayoutUnit inline_size;
|
| + while (true) {
|
| + unsigned next_break = break_iterator.NextBreakOpportunity(offset + 1);
|
| + LayoutUnit next_inline_size =
|
| + inline_size +
|
| + item.InlineSize(offset, std::min(next_break, item.EndOffset()));
|
| + if (next_inline_size > available_width) {
|
| + if (!has_break_opportunities)
|
| + return std::make_pair(next_break, next_inline_size);
|
| + return std::make_pair(offset, inline_size);
|
| }
|
| + if (next_break >= item.EndOffset())
|
| + return std::make_pair(next_break, next_inline_size);
|
| + offset = next_break;
|
| + inline_size = next_inline_size;
|
| + has_break_opportunities = true;
|
| + }
|
| +}
|
| +#endif
|
| +
|
| +LineBreakType GetLineBreakType(const ComputedStyle& style) {
|
| + if (style.AutoWrap()) {
|
| + if (style.WordBreak() == kBreakAllWordBreak ||
|
| + style.WordBreak() == kBreakWordBreak)
|
| + return LineBreakType::kBreakAll;
|
| + if (style.WordBreak() == kKeepAllWordBreak)
|
| + return LineBreakType::kKeepAll;
|
| + }
|
| + return LineBreakType::kNormal;
|
| +}
|
| +
|
| +} // namespace
|
| +
|
| +NGLineBreaker::NGLineBreaker(NGInlineNode* node,
|
| + const NGConstraintSpace* space,
|
| + NGInlineBreakToken* break_token)
|
| + : node_(node), constraint_space_(space), item_index_(0), offset_(0) {
|
| + if (break_token) {
|
| + item_index_ = break_token->ItemIndex();
|
| + offset_ = break_token->TextOffset();
|
| + node->AssertOffset(item_index_, offset_);
|
| + }
|
| +}
|
|
|
| - // Compute hangable characters if exists.
|
| - if (current_offset != start_of_hangables) {
|
| - algorithm->SetStartOfHangables(start_of_hangables);
|
| - // If text before hangables can fit, include it in the current line.
|
| - if (algorithm->CanFitOnLine())
|
| - algorithm->SetBreakOpportunity();
|
| +void NGLineBreaker::NextLine(NGInlineItemResults* item_results,
|
| + NGInlineLayoutAlgorithm* algorithm) {
|
| + BreakLine(item_results, algorithm);
|
| +
|
| + // TODO(kojii): When editing, or caret is enabled, trailing spaces at wrap
|
| + // point should not be removed. For other cases, we can a) remove, b) leave
|
| + // characters without glyphs, or c) leave both characters and glyphs without
|
| + // measuring. Need to decide which one works the best.
|
| + SkipCollapsibleWhitespaces();
|
| +}
|
| +
|
| +void NGLineBreaker::BreakLine(NGInlineItemResults* item_results,
|
| + NGInlineLayoutAlgorithm* algorithm) {
|
| + DCHECK(item_results->IsEmpty());
|
| + const Vector<NGInlineItem>& items = node_->Items();
|
| + const String& text = node_->Text();
|
| + const ComputedStyle& style = node_->Style();
|
| + LazyLineBreakIterator break_iterator(text, style.LocaleForLineBreakIterator(),
|
| + GetLineBreakType(style));
|
| +#if !defined(MOCK_SHAPE_LINE)
|
| + HarfBuzzShaper shaper(text.Characters16(), text.length());
|
| +#endif
|
| + LayoutUnit available_width = algorithm->AvailableWidth();
|
| + LayoutUnit position;
|
| + while (item_index_ < items.size()) {
|
| + const NGInlineItem& item = items[item_index_];
|
| + item_results->push_back(
|
| + NGInlineItemResult(item_index_, offset_, item.EndOffset()));
|
| + NGInlineItemResult* item_result = &item_results->back();
|
| +
|
| + // If the start offset is at the item boundary, try to add the entire item.
|
| + if (offset_ == item.StartOffset()) {
|
| + if (item.Type() == NGInlineItem::kText) {
|
| + item_result->inline_size = item.InlineSize();
|
| + } else if (item.Type() == NGInlineItem::kAtomicInline) {
|
| + LayoutAtomicInline(item, item_result);
|
| + } else if (item.Type() == NGInlineItem::kFloating) {
|
| + algorithm->LayoutAndPositionFloat(position, item.GetLayoutObject());
|
| + // Floats may change the available width if they fit.
|
| + available_width = algorithm->AvailableWidth();
|
| + // Floats are already positioned in the container_builder.
|
| + item_results->pop_back();
|
| + offset_ = item.EndOffset();
|
| + item_index_++;
|
| + continue;
|
| + } else {
|
| + offset_ = item.EndOffset();
|
| + item_index_++;
|
| + continue;
|
| + }
|
| + LayoutUnit next_position = position + item_result->inline_size;
|
| + if (next_position <= available_width) {
|
| + offset_ = item.EndOffset();
|
| + item_index_++;
|
| + position = next_position;
|
| + continue;
|
| + }
|
| +
|
| + // The entire item does not fit. Handle non-text items as overflow,
|
| + // since only text item is breakable.
|
| + if (item.Type() != NGInlineItem::kText) {
|
| + offset_ = item.EndOffset();
|
| + item_index_++;
|
| + return HandleOverflow(item_results, break_iterator);
|
| + }
|
| }
|
|
|
| - if (!algorithm->HasBreakOpportunity()) {
|
| - // The first word (break opportunity) did not fit on the line.
|
| - // Create a line including items that don't fit, allowing them to
|
| - // overflow.
|
| - if (!algorithm->CreateLine())
|
| - return;
|
| + // Either the start or the break is in the mid of a text item.
|
| + DCHECK_EQ(item.Type(), NGInlineItem::kText);
|
| + DCHECK_LT(offset_, item.EndOffset());
|
| + break_iterator.SetLocale(item.Style()->LocaleForLineBreakIterator());
|
| + break_iterator.SetBreakType(GetLineBreakType(*item.Style()));
|
| +#if defined(MOCK_SHAPE_LINE)
|
| + unsigned break_offset;
|
| + std::tie(break_offset, item_result->inline_size) = ShapeLineMock(
|
| + item, offset_, available_width - position, break_iterator);
|
| +#else
|
| + // TODO(kojii): We need to instantiate ShapingLineBreaker here because it
|
| + // has item-specific info as context. Should they be part of ShapeLine() to
|
| + // instantiate once, or is this just fine since instatiation is not
|
| + // expensive?
|
| + DCHECK_EQ(item.TextShapeResult()->StartIndexForResult(),
|
| + item.StartOffset());
|
| + DCHECK_EQ(item.TextShapeResult()->EndIndexForResult(), item.EndOffset());
|
| + ShapingLineBreaker breaker(&shaper, &item.Style()->GetFont(),
|
| + item.TextShapeResult(), &break_iterator);
|
| + unsigned break_offset;
|
| + item_result->shape_result =
|
| + breaker.ShapeLine(offset_, available_width - position, &break_offset);
|
| + item_result->inline_size = item_result->shape_result->SnappedWidth();
|
| +#endif
|
| + DCHECK_GT(break_offset, offset_);
|
| + position += item_result->inline_size;
|
| +
|
| + // If the break found within the item, break here.
|
| + if (break_offset < item.EndOffset()) {
|
| + offset_ = item_result->end_offset = break_offset;
|
| + if (position <= available_width)
|
| + break;
|
| + // The first break opportunity of the item does not fit.
|
| } else {
|
| - if (!algorithm->CreateLineUpToLastBreakOpportunity())
|
| - return;
|
| -
|
| - // Items after the last break opportunity were sent to the next line.
|
| - // Set the break opportunity, or create a line if the word doesn't fit.
|
| - if (algorithm->HasItems()) {
|
| - if (algorithm->CanFitOnLine())
|
| - algorithm->SetBreakOpportunity();
|
| - else if (!algorithm->CreateLine())
|
| - return;
|
| - }
|
| + // No break opporunity in the item, or the first break opportunity is at
|
| + // the end of the item. If it fits, continue to the next item.
|
| + offset_ = item_result->end_offset = item.EndOffset();
|
| + item_index_++;
|
| + if (position <= available_width)
|
| + continue;
|
| + }
|
| +
|
| + // We need to look at next item if we're overflowing, and the break
|
| + // opportunity is beyond this item.
|
| + if (break_offset > item.EndOffset())
|
| + continue;
|
| + return HandleOverflow(item_results, break_iterator);
|
| + }
|
| +}
|
| +
|
| +void NGLineBreaker::LayoutAtomicInline(const NGInlineItem& item,
|
| + NGInlineItemResult* item_result) {
|
| + DCHECK_EQ(item.Type(), NGInlineItem::kAtomicInline);
|
| + NGBlockNode* node = new NGBlockNode(item.GetLayoutObject());
|
| + const ComputedStyle& style = node->Style();
|
| + NGConstraintSpaceBuilder constraint_space_builder(constraint_space_);
|
| + RefPtr<NGConstraintSpace> constraint_space =
|
| + constraint_space_builder.SetIsNewFormattingContext(true)
|
| + .SetIsShrinkToFit(true)
|
| + .SetTextDirection(style.Direction())
|
| + .ToConstraintSpace(FromPlatformWritingMode(style.GetWritingMode()));
|
| + item_result->layout_result = node->Layout(constraint_space.Get());
|
| +
|
| + item_result->inline_size =
|
| + NGBoxFragment(constraint_space_->WritingMode(),
|
| + ToNGPhysicalBoxFragment(
|
| + item_result->layout_result->PhysicalFragment().Get()))
|
| + .InlineSize();
|
| +}
|
| +
|
| +// Handles when the last item overflows.
|
| +// At this point, item_results does not fit into the current line, and there
|
| +// are no break opportunities in item_results.back().
|
| +void NGLineBreaker::HandleOverflow(
|
| + NGInlineItemResults* item_results,
|
| + const LazyLineBreakIterator& break_iterator) {
|
| + DCHECK_GT(offset_, 0u);
|
| +
|
| + // Find the last break opportunity. If none, let this line overflow.
|
| + unsigned line_start_offset = item_results->front().start_offset;
|
| + unsigned break_offset =
|
| + break_iterator.PreviousBreakOpportunity(offset_ - 1, line_start_offset);
|
| + if (!break_offset || break_offset <= line_start_offset) {
|
| + AppendCloseTags(item_results);
|
| + return;
|
| + }
|
| +
|
| + // Truncate the end of the line to the break opportunity.
|
| + const Vector<NGInlineItem>& items = node_->Items();
|
| + unsigned new_end = item_results->size();
|
| + while (true) {
|
| + NGInlineItemResult* item_result = &(*item_results)[--new_end];
|
| + if (item_result->start_offset < break_offset) {
|
| + // The break is at the mid of the item. Adjust the end_offset to the new
|
| + // break offset.
|
| + const NGInlineItem& item = items[item_result->item_index];
|
| + item.AssertEndOffset(break_offset);
|
| + DCHECK_EQ(item.Type(), NGInlineItem::kText);
|
| + DCHECK_NE(item_result->end_offset, break_offset);
|
| + item_result->end_offset = break_offset;
|
| + item_result->inline_size =
|
| + item.InlineSize(item_result->start_offset, item_result->end_offset);
|
| + // TODO(kojii): May need to reshape. Add to ShapingLineBreaker?
|
| + new_end++;
|
| + break;
|
| + }
|
| + if (item_result->start_offset == break_offset) {
|
| + // The new break offset is at the item boundary. Remove items up to the
|
| + // new break offset.
|
| + // TODO(kojii): Remove open tags as well.
|
| + break;
|
| }
|
| }
|
| + DCHECK_GT(new_end, 0u);
|
| +
|
| + // TODO(kojii): Should we keep results for the next line? We don't need to
|
| + // re-layout atomic inlines.
|
| + // TODO(kojii): Removing processed floats is likely a problematic. Keep
|
| + // floats in this line, or keep it for the next line.
|
| + item_results->Shrink(new_end);
|
| +
|
| + // Update the current item index and offset to the new break point.
|
| + const NGInlineItemResult& last_item_result = item_results->back();
|
| + offset_ = last_item_result.end_offset;
|
| + item_index_ = last_item_result.item_index;
|
| + if (items[item_index_].EndOffset() == offset_)
|
| + item_index_++;
|
| +}
|
| +
|
| +void NGLineBreaker::SkipCollapsibleWhitespaces() {
|
| + const Vector<NGInlineItem>& items = node_->Items();
|
| + if (item_index_ >= items.size())
|
| + return;
|
| + const NGInlineItem& item = items[item_index_];
|
| + if (item.Type() != NGInlineItem::kText || !item.Style()->CollapseWhiteSpace())
|
| + return;
|
| +
|
| + DCHECK_LT(offset_, item.EndOffset());
|
| + if (node_->Text()[offset_] == kSpaceCharacter) {
|
| + // Skip one whitespace. Collapsible spaces are collapsed to single space in
|
| + // NGInlineItemBuilder, so this removes all collapsible spaces.
|
| + offset_++;
|
| + if (offset_ == item.EndOffset())
|
| + item_index_++;
|
| + }
|
| +}
|
| +
|
| +void NGLineBreaker::AppendCloseTags(NGInlineItemResults* item_results) {
|
| + const Vector<NGInlineItem>& items = node_->Items();
|
| + for (; item_index_ < items.size(); item_index_++) {
|
| + const NGInlineItem& item = items[item_index_];
|
| + if (item.Type() != NGInlineItem::kCloseTag)
|
| + break;
|
| + DCHECK_EQ(offset_, item.EndOffset());
|
| + item_results->push_back(NGInlineItemResult(item_index_, offset_, offset_));
|
| + }
|
| +}
|
|
|
| - // If inline children ended with items left in the line builder, create a line
|
| - // for them.
|
| - if (algorithm->HasItems())
|
| - algorithm->CreateLine();
|
| +RefPtr<NGInlineBreakToken> NGLineBreaker::CreateBreakToken() const {
|
| + const Vector<NGInlineItem>& items = node_->Items();
|
| + if (item_index_ >= items.size())
|
| + return nullptr;
|
| + return NGInlineBreakToken::Create(node_, item_index_, offset_);
|
| }
|
|
|
| } // namespace blink
|
|
|