| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // 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_line_breaker.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" |
| #include "third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.h" |
| #include "third_party/blink/renderer/core/layout/ng/ng_box_fragment.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_floats_utils.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/style/computed_style.h" |
| #include "third_party/blink/renderer/platform/fonts/shaping/shaping_line_breaker.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // CSS-defined white space characters, excluding the newline character. |
| // In most cases, the line breaker consider break opportunities are before |
| // spaces because it handles trailing spaces differently from other normal |
| // characters, but breaking before newline characters is not desired. |
| inline bool IsBreakableSpace(UChar c) { |
| return c == kSpaceCharacter || c == kTabulationCharacter; |
| } |
| |
| inline bool CanBreakAfterLast(const NGInlineItemResults& item_results) { |
| return !item_results.IsEmpty() && item_results.back().can_break_after; |
| } |
| |
| } // namespace |
| |
| NGLineBreaker::LineData::LineData(NGInlineNode node, |
| const NGInlineBreakToken* break_token) { |
| is_first_formatted_line = (!break_token || (!break_token->ItemIndex() && |
| !break_token->TextOffset())) && |
| node.CanContainFirstFormattedLine(); |
| use_first_line_style = is_first_formatted_line && node.GetLayoutObject() |
| ->GetDocument() |
| .GetStyleEngine() |
| .UsesFirstLineRules(); |
| } |
| |
| NGLineBreaker::NGLineBreaker( |
| NGInlineNode node, |
| NGLineBreakerMode mode, |
| const NGConstraintSpace& space, |
| Vector<NGPositionedFloat>* positioned_floats, |
| Vector<scoped_refptr<NGUnpositionedFloat>>* unpositioned_floats, |
| NGContainerFragmentBuilder* container_builder, |
| NGExclusionSpace* exclusion_space, |
| unsigned handled_float_index, |
| const NGInlineBreakToken* break_token) |
| : line_(node, break_token), |
| node_(node), |
| items_data_(node.ItemsData(line_.use_first_line_style)), |
| mode_(mode), |
| constraint_space_(space), |
| positioned_floats_(positioned_floats), |
| unpositioned_floats_(unpositioned_floats), |
| container_builder_(container_builder), |
| exclusion_space_(exclusion_space), |
| break_iterator_(items_data_.text_content), |
| shaper_(items_data_.text_content.Characters16(), |
| items_data_.text_content.length()), |
| spacing_(items_data_.text_content), |
| handled_floats_end_item_index_(handled_float_index), |
| base_direction_(node_.BaseDirection()), |
| in_line_height_quirks_mode_(node.InLineHeightQuirksMode()) { |
| break_iterator_.SetBreakSpace(BreakSpaceType::kBeforeSpaceRun); |
| |
| if (break_token) { |
| current_style_ = break_token->Style(); |
| item_index_ = break_token->ItemIndex(); |
| offset_ = break_token->TextOffset(); |
| previous_line_had_forced_break_ = break_token->IsForcedBreak(); |
| items_data_.AssertOffset(item_index_, offset_); |
| ignore_floats_ = break_token->IgnoreFloats(); |
| } |
| } |
| |
| inline NGInlineItemResult* NGLineBreaker::AddItem( |
| const NGInlineItem& item, |
| unsigned end_offset, |
| NGInlineItemResults* item_results) { |
| DCHECK_LE(end_offset, item.EndOffset()); |
| item_results->push_back( |
| NGInlineItemResult(&item, item_index_, offset_, end_offset)); |
| return &item_results->back(); |
| } |
| |
| inline NGInlineItemResult* NGLineBreaker::AddItem( |
| const NGInlineItem& item, |
| NGInlineItemResults* item_results) { |
| return AddItem(item, item.EndOffset(), item_results); |
| } |
| |
| void NGLineBreaker::SetLineEndFragment( |
| scoped_refptr<NGPhysicalTextFragment> fragment, |
| NGLineInfo* line_info) { |
| bool is_horizontal = |
| IsHorizontalWritingMode(constraint_space_.GetWritingMode()); |
| if (line_info->LineEndFragment()) { |
| const NGPhysicalSize& size = line_info->LineEndFragment()->Size(); |
| line_.position -= is_horizontal ? size.width : size.height; |
| } |
| if (fragment) { |
| const NGPhysicalSize& size = fragment->Size(); |
| line_.position += is_horizontal ? size.width : size.height; |
| } |
| line_info->SetLineEndFragment(std::move(fragment)); |
| } |
| |
| inline void NGLineBreaker::ComputeCanBreakAfter( |
| NGInlineItemResult* item_result) const { |
| item_result->can_break_after = |
| auto_wrap_ && break_iterator_.IsBreakable(item_result->end_offset); |
| } |
| |
| // True if |item| is trailing; i.e., |item| and all items after it are opaque to |
| // whitespace collapsing. |
| bool NGLineBreaker::IsTrailing(const NGInlineItem& item, |
| const NGLineInfo& line_info) const { |
| const Vector<NGInlineItem>& items = line_info.ItemsData().items; |
| for (const NGInlineItem* it = &item; it != items.end(); ++it) { |
| if (it->EndCollapseType() != NGInlineItem::kOpaqueToCollapsing) |
| return false; |
| } |
| return true; |
| } |
| |
| // Compute the base direction for bidi algorithm for this line. |
| void NGLineBreaker::ComputeBaseDirection(const NGLineInfo& line_info) { |
| // If 'unicode-bidi' is not 'plaintext', use the base direction of the block. |
| if (!previous_line_had_forced_break_ || |
| node_.Style().GetUnicodeBidi() != UnicodeBidi::kPlaintext) |
| return; |
| // If 'unicode-bidi: plaintext', compute the base direction for each paragraph |
| // (separated by forced break.) |
| const String& text = line_info.ItemsData().text_content; |
| if (text.Is8Bit()) |
| return; |
| size_t end_offset = text.find(kNewlineCharacter, offset_); |
| base_direction_ = NGBidiParagraph::BaseDirectionForString( |
| end_offset == kNotFound |
| ? StringView(text, offset_) |
| : StringView(text, offset_, end_offset - offset_)); |
| } |
| |
| // Initialize internal states for the next line. |
| void NGLineBreaker::PrepareNextLine( |
| const NGLineLayoutOpportunity& line_opportunity, |
| NGLineInfo* line_info) { |
| NGInlineItemResults* item_results = &line_info->Results(); |
| item_results->clear(); |
| line_info->SetStartOffset(offset_); |
| line_info->SetLineStyle( |
| node_, items_data_, constraint_space_, line_.is_first_formatted_line, |
| line_.use_first_line_style, previous_line_had_forced_break_); |
| // Set the initial style of this line from the break token. Example: |
| // <p>...<span>....</span></p> |
| // When the line wraps in <span>, the 2nd line needs to start with the style |
| // of the <span>. |
| override_break_anywhere_ = false; |
| SetCurrentStyle(current_style_ ? *current_style_ : line_info->LineStyle()); |
| ComputeBaseDirection(*line_info); |
| line_info->SetBaseDirection(base_direction_); |
| |
| line_.is_after_forced_break = false; |
| line_.should_create_line_box = false; |
| |
| // Use 'text-indent' as the initial position. This lets tab positions to align |
| // regardless of 'text-indent'. |
| line_.position = line_info->TextIndent(); |
| |
| line_.line_opportunity = line_opportunity; |
| } |
| |
| bool NGLineBreaker::NextLine(const NGLineLayoutOpportunity& line_opportunity, |
| NGLineInfo* line_info) { |
| PrepareNextLine(line_opportunity, line_info); |
| BreakLine(line_info); |
| |
| if (line_info->Results().IsEmpty()) |
| return false; |
| |
| // TODO(kojii): There are cases where we need to PlaceItems() without creating |
| // line boxes. These cases need to be reviewed. |
| if (line_.should_create_line_box) |
| ComputeLineLocation(line_info); |
| |
| return true; |
| } |
| |
| void NGLineBreaker::BreakLine(NGLineInfo* line_info) { |
| NGInlineItemResults* item_results = &line_info->Results(); |
| const Vector<NGInlineItem>& items = line_info->ItemsData().items; |
| LineBreakState state = LineBreakState::kContinue; |
| while (state != LineBreakState::kDone) { |
| // Check overflow even if |item_index_| is at the end of the block, because |
| // the last item of the block may have caused overflow. In that case, |
| // |HandleOverflow| will rewind |item_index_|. |
| if (state == LineBreakState::kContinue && auto_wrap_ && !line_.CanFit()) { |
| state = HandleOverflow(line_info); |
| } |
| |
| // If we reach at the end of the block, this is the last line. |
| DCHECK_LE(item_index_, items.size()); |
| if (item_index_ == items.size()) { |
| RemoveTrailingCollapsibleSpace(line_info); |
| line_info->SetIsLastLine(true); |
| return; |
| } |
| |
| // Handle trailable items first. These items may not be break before. |
| // They (or part of them) may also overhang the available width. |
| const NGInlineItem& item = items[item_index_]; |
| if (item.Type() == NGInlineItem::kText) { |
| state = HandleText(item, state, line_info); |
| #if DCHECK_IS_ON() |
| if (!item_results->IsEmpty()) |
| item_results->back().CheckConsistency(); |
| #endif |
| continue; |
| } |
| if (item.Type() == NGInlineItem::kCloseTag) { |
| HandleCloseTag(item, item_results); |
| continue; |
| } |
| if (item.Type() == NGInlineItem::kControl) { |
| state = HandleControlItem(item, state, line_info); |
| continue; |
| } |
| if (item.Type() == NGInlineItem::kBidiControl) { |
| state = HandleBidiControlItem(item, state, line_info); |
| continue; |
| } |
| |
| // Items after this point are not trailable. Break at the earliest break |
| // opportunity if we're trailing. |
| if (state == LineBreakState::kTrailing && |
| CanBreakAfterLast(*item_results)) { |
| line_info->SetIsLastLine(false); |
| return; |
| } |
| |
| if (item.Type() == NGInlineItem::kAtomicInline) { |
| HandleAtomicInline(item, line_info); |
| } else if (item.Type() == NGInlineItem::kOpenTag) { |
| HandleOpenTag(item, AddItem(item, item_results)); |
| } else if (item.Type() == NGInlineItem::kFloating) { |
| HandleFloat(item, line_info, AddItem(item, item_results)); |
| } else if (item.Type() == NGInlineItem::kOutOfFlowPositioned) { |
| DCHECK_EQ(item.Length(), 0u); |
| AddItem(item, item_results); |
| MoveToNextOf(item); |
| } else if (item.Length()) { |
| NOTREACHED(); |
| // For other items with text (e.g., bidi controls), use their text to |
| // determine the break opportunity. |
| NGInlineItemResult* item_result = AddItem(item, item_results); |
| item_result->can_break_after = |
| break_iterator_.IsBreakable(item_result->end_offset); |
| MoveToNextOf(item); |
| } else if (item.Type() == NGInlineItem::kListMarker) { |
| line_.should_create_line_box = true; |
| NGInlineItemResult* item_result = AddItem(item, item_results); |
| DCHECK(!item_result->can_break_after); |
| MoveToNextOf(item); |
| } else { |
| NOTREACHED(); |
| MoveToNextOf(item); |
| } |
| } |
| } |
| |
| // Re-compute the current position from NGInlineItemResults. |
| // The current position is usually updated as NGLineBreaker builds |
| // NGInlineItemResults. This function re-computes it when it was lost. |
| void NGLineBreaker::UpdatePosition(const NGInlineItemResults& results) { |
| LayoutUnit position; |
| for (const NGInlineItemResult& item_result : results) |
| position += item_result.inline_size; |
| line_.position = position; |
| } |
| |
| void NGLineBreaker::ComputeLineLocation(NGLineInfo* line_info) const { |
| LayoutUnit bfc_line_offset = line_.line_opportunity.line_left_offset; |
| LayoutUnit available_width = line_.AvailableWidth(); |
| |
| // Negative margins can make the position negative, but the inline size is |
| // always positive or 0. |
| line_info->SetLineBfcOffset( |
| {bfc_line_offset, line_.line_opportunity.bfc_block_offset}, |
| available_width, line_.position.ClampNegativeToZero()); |
| } |
| |
| NGLineBreaker::LineBreakState NGLineBreaker::HandleText( |
| const NGInlineItem& item, |
| LineBreakState state, |
| NGLineInfo* line_info) { |
| DCHECK_EQ(item.Type(), NGInlineItem::kText); |
| DCHECK(item.TextShapeResult()); |
| NGInlineItemResults* item_results = &line_info->Results(); |
| |
| // If we're trailing, only trailing spaces can be included in this line. |
| if (state == LineBreakState::kTrailing && CanBreakAfterLast(*item_results)) { |
| return HandleTrailingSpaces(item, line_info); |
| } |
| |
| line_.should_create_line_box = true; |
| NGInlineItemResult* item_result = AddItem(item, item_results); |
| LayoutUnit available_width = line_.AvailableWidth(); |
| |
| if (auto_wrap_) { |
| // Try to break inside of this text item. |
| BreakText(item_result, item, available_width - line_.position, line_info); |
| LayoutUnit next_position = line_.position + item_result->inline_size; |
| bool is_overflow = next_position > available_width; |
| line_.position = next_position; |
| item_result->may_break_inside = !is_overflow; |
| MoveToNextOf(*item_result); |
| |
| if (!is_overflow || state == LineBreakState::kTrailing) { |
| if (item_result->end_offset < item.EndOffset()) { |
| // The break point found, and text follows. Break here, after trailing |
| // spaces. |
| return HandleTrailingSpaces(item, line_info); |
| } |
| |
| // The break point found, but items that prohibit breaking before them may |
| // follow. Continue looking next items. |
| return state; |
| } |
| |
| return HandleOverflow(line_info); |
| } |
| |
| // Add the rest of the item if !auto_wrap. |
| // Because the start position may need to reshape, run ShapingLineBreaker |
| // with max available width. |
| BreakText(item_result, item, LayoutUnit::Max(), line_info); |
| DCHECK_EQ(item_result->end_offset, item.EndOffset()); |
| DCHECK(!item_result->may_break_inside); |
| item_result->can_break_after = false; |
| line_.position += item_result->inline_size; |
| MoveToNextOf(item); |
| return state; |
| } |
| |
| void NGLineBreaker::BreakText(NGInlineItemResult* item_result, |
| const NGInlineItem& item, |
| LayoutUnit available_width, |
| NGLineInfo* line_info) { |
| DCHECK_EQ(item.Type(), NGInlineItem::kText); |
| item.AssertOffset(item_result->start_offset); |
| |
| // 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_, |
| &spacing_, hyphenation_); |
| if (!enable_soft_hyphen_) |
| breaker.DisableSoftHyphen(); |
| available_width = std::max(LayoutUnit(0), available_width); |
| ShapingLineBreaker::Result result; |
| scoped_refptr<ShapeResult> shape_result = |
| breaker.ShapeLine(item_result->start_offset, available_width, |
| offset_ == line_info->StartOffset(), &result); |
| DCHECK_GT(shape_result->NumCharacters(), 0u); |
| if (result.is_hyphenated) { |
| AppendHyphen(item, line_info); |
| // TODO(kojii): Implement when adding a hyphen caused overflow. |
| // crbug.com/714962: Should be removed when switched to NGPaint. |
| item_result->text_end_effect = NGTextEndEffect::kHyphen; |
| } |
| item_result->inline_size = shape_result->SnappedWidth().ClampNegativeToZero(); |
| item_result->end_offset = result.break_offset; |
| item_result->shape_result = std::move(shape_result); |
| DCHECK_GT(item_result->end_offset, item_result->start_offset); |
| |
| // * If width <= available_width: |
| // * If offset < item.EndOffset(): the break opportunity to fit is found. |
| // * If offset == item.EndOffset(): the break opportunity at the end fits, |
| // or the first break opportunity is beyond the end. |
| // There may be room for more characters. |
| // * If width > available_width: The first break opportunity does not fit. |
| // offset is the first break opportunity, either inside, at the end, or |
| // beyond the end. |
| if (item_result->end_offset < item.EndOffset()) { |
| item_result->can_break_after = true; |
| } else { |
| DCHECK_EQ(item_result->end_offset, item.EndOffset()); |
| item_result->can_break_after = |
| break_iterator_.IsBreakable(item_result->end_offset); |
| } |
| } |
| |
| NGLineBreaker::LineBreakState NGLineBreaker::HandleTrailingSpaces( |
| const NGInlineItem& item, |
| NGLineInfo* line_info) { |
| DCHECK_EQ(item.Type(), NGInlineItem::kText); |
| DCHECK_LT(offset_, item.EndOffset()); |
| const String& text = Text(); |
| NGInlineItemResults* item_results = &line_info->Results(); |
| DCHECK(item.Style()); |
| const ComputedStyle& style = *item.Style(); |
| if (style.CollapseWhiteSpace()) { |
| if (text[offset_] != kSpaceCharacter) |
| return LineBreakState::kDone; |
| |
| // Skipping one whitespace removes all collapsible spaces because |
| // collapsible spaces are collapsed to single space in NGInlineItemBuilder. |
| offset_++; |
| |
| // Make the last item breakable after, even if it was nowrap. |
| DCHECK(!item_results->IsEmpty()); |
| item_results->back().can_break_after = true; |
| } else { |
| // Find the end of the run of space characters in this item. |
| // Other white space characters (e.g., tab) are not included in this item. |
| DCHECK(style.BreakOnlyAfterWhiteSpace()); |
| unsigned end = offset_; |
| while (end < item.EndOffset() && text[end] == kSpaceCharacter) |
| end++; |
| if (end == offset_) |
| return LineBreakState::kDone; |
| |
| NGInlineItemResult* item_result = AddItem(item, end, item_results); |
| item_result->has_only_trailing_spaces = true; |
| // TODO(kojii): Should reshape if it's not safe to break. |
| item_result->shape_result = item.TextShapeResult()->SubRange(offset_, end); |
| item_result->inline_size = item_result->shape_result->SnappedWidth(); |
| line_.position += item_result->inline_size; |
| item_result->can_break_after = |
| end < text.length() && !IsBreakableSpace(text[end]); |
| offset_ = end; |
| } |
| |
| // If non-space characters follow, the line is done. |
| // Otherwise keep checking next items for the break point. |
| DCHECK_LE(offset_, item.EndOffset()); |
| if (offset_ < item.EndOffset()) |
| return LineBreakState::kDone; |
| item_index_++; |
| return 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) { |
| NGInlineItemResults* item_results = &line_info->Results(); |
| if (item_results->IsEmpty()) |
| return; |
| for (auto it = item_results->rbegin(); it != item_results->rend(); ++it) { |
| NGInlineItemResult& item_result = *it; |
| DCHECK(item_result.item); |
| const NGInlineItem& item = *item_result.item; |
| if (item.EndCollapseType() == NGInlineItem::kOpaqueToCollapsing) |
| continue; |
| if (item.Type() != NGInlineItem::kText) |
| return; |
| const String& text = Text(); |
| if (text[item_result.end_offset - 1] != kSpaceCharacter) |
| return; |
| DCHECK(item.Style()); |
| if (!item.Style()->CollapseWhiteSpace()) |
| return; |
| |
| // We have a trailing collapsible space. Remove it. |
| line_.position -= item_result.inline_size; |
| --item_result.end_offset; |
| if (item_result.end_offset == item_result.start_offset) { |
| unsigned index = std::distance(item_results->begin(), &item_result); |
| item_results->EraseAt(index); |
| } else { |
| // TODO(kojii): Should reshape if it's not safe to break. |
| item_result.shape_result = item_result.shape_result->SubRange( |
| item_result.start_offset, item_result.end_offset); |
| item_result.inline_size = item_result.shape_result->SnappedWidth(); |
| line_.position += item_result.inline_size; |
| } |
| return; |
| } |
| } |
| |
| void NGLineBreaker::AppendHyphen(const NGInlineItem& item, |
| NGLineInfo* line_info) { |
| DCHECK(item.Style()); |
| const ComputedStyle& style = *item.Style(); |
| TextDirection direction = style.Direction(); |
| String hyphen_string = style.HyphenString(); |
| hyphen_string.Ensure16Bit(); |
| HarfBuzzShaper shaper(hyphen_string.Characters16(), hyphen_string.length()); |
| scoped_refptr<ShapeResult> hyphen_result = |
| shaper.Shape(&style.GetFont(), direction); |
| NGTextFragmentBuilder builder(node_, constraint_space_.GetWritingMode()); |
| builder.SetText(item.GetLayoutObject(), hyphen_string, &style, |
| /* is_ellipsis_style */ false, std::move(hyphen_result)); |
| SetLineEndFragment(builder.ToTextFragment(), line_info); |
| } |
| |
| // Measure control items; new lines and tab, that are similar to text, affect |
| // layout, but do not need shaping/painting. |
| NGLineBreaker::LineBreakState NGLineBreaker::HandleControlItem( |
| const NGInlineItem& item, |
| LineBreakState state, |
| NGLineInfo* line_info) { |
| DCHECK_EQ(item.Length(), 1u); |
| line_.should_create_line_box = true; |
| |
| UChar character = Text()[item.StartOffset()]; |
| switch (character) { |
| case kNewlineCharacter: { |
| NGInlineItemResult* item_result = AddItem(item, &line_info->Results()); |
| item_result->has_only_trailing_spaces = true; |
| line_.is_after_forced_break = true; |
| line_info->SetIsLastLine(true); |
| state = LineBreakState::kDone; |
| break; |
| } |
| case kTabulationCharacter: { |
| NGInlineItemResult* item_result = AddItem(item, &line_info->Results()); |
| DCHECK(item.Style()); |
| const ComputedStyle& style = *item.Style(); |
| const Font& font = style.GetFont(); |
| item_result->inline_size = |
| font.TabWidth(style.GetTabSize(), line_.position); |
| line_.position += item_result->inline_size; |
| item_result->has_only_trailing_spaces = |
| state == LineBreakState::kTrailing; |
| ComputeCanBreakAfter(item_result); |
| break; |
| } |
| case kZeroWidthSpaceCharacter: { |
| // <wbr> tag creates break opportunities regardless of auto_wrap. |
| NGInlineItemResult* item_result = AddItem(item, &line_info->Results()); |
| item_result->can_break_after = true; |
| break; |
| } |
| case kCarriageReturnCharacter: |
| case kFormFeedCharacter: |
| // Ignore carriage return and form feed. |
| // https://drafts.csswg.org/css-text-3/#white-space-processing |
| // https://github.com/w3c/csswg-drafts/issues/855 |
| break; |
| default: |
| NOTREACHED(); |
| break; |
| } |
| MoveToNextOf(item); |
| return state; |
| } |
| |
| NGLineBreaker::LineBreakState NGLineBreaker::HandleBidiControlItem( |
| const NGInlineItem& item, |
| LineBreakState state, |
| NGLineInfo* line_info) { |
| DCHECK_EQ(item.Length(), 1u); |
| NGInlineItemResults* item_results = &line_info->Results(); |
| |
| // Bidi control characters have enter/exit semantics. Handle "enter" |
| // characters simialr to open-tag, while "exit" (pop) characters similar to |
| // close-tag. |
| UChar character = Text()[item.StartOffset()]; |
| bool is_pop = character == kPopDirectionalIsolateCharacter || |
| character == kPopDirectionalFormattingCharacter; |
| if (is_pop) { |
| if (!item_results->IsEmpty()) { |
| NGInlineItemResult* item_result = AddItem(item, item_results); |
| NGInlineItemResult* last = &(*item_results)[item_results->size() - 2]; |
| item_result->can_break_after = last->can_break_after; |
| last->can_break_after = false; |
| } else { |
| AddItem(item, item_results); |
| } |
| } else { |
| if (state == LineBreakState::kTrailing && |
| CanBreakAfterLast(*item_results)) { |
| line_info->SetIsLastLine(false); |
| MoveToNextOf(item); |
| return LineBreakState::kDone; |
| } |
| NGInlineItemResult* item_result = AddItem(item, item_results); |
| DCHECK(!item_result->can_break_after); |
| } |
| MoveToNextOf(item); |
| return state; |
| } |
| |
| void NGLineBreaker::HandleAtomicInline(const NGInlineItem& item, |
| NGLineInfo* line_info) { |
| DCHECK_EQ(item.Type(), NGInlineItem::kAtomicInline); |
| line_.should_create_line_box = true; |
| |
| NGInlineItemResult* item_result = AddItem(item, &line_info->Results()); |
| item_result->layout_result = |
| NGBlockNode(ToLayoutBox(item.GetLayoutObject())) |
| .LayoutAtomicInline(constraint_space_, |
| line_info->UseFirstLineStyle()); |
| DCHECK(item_result->layout_result->PhysicalFragment()); |
| |
| item_result->inline_size = |
| NGFragment(constraint_space_.GetWritingMode(), |
| *item_result->layout_result->PhysicalFragment()) |
| .InlineSize(); |
| |
| DCHECK(item.Style()); |
| item_result->margins = |
| ComputeMarginsForVisualContainer(constraint_space_, *item.Style()); |
| item_result->padding = ComputePadding(constraint_space_, *item.Style()); |
| item_result->inline_size += item_result->margins.InlineSum(); |
| |
| line_.position += item_result->inline_size; |
| ComputeCanBreakAfter(item_result); |
| MoveToNextOf(item); |
| } |
| |
| // Performs layout and positions a float. |
| // |
| // If there is a known available_width (e.g. something has resolved the |
| // container BFC offset) it will attempt to position the float on the current |
| // line. |
| // Additionally updates the available_width for the line as the float has |
| // (probably) consumed space. |
| // |
| // If the float is too wide *or* we already have UnpositionedFloats we add it |
| // as an UnpositionedFloat. This should be positioned *immediately* after we |
| // are done with the current line. |
| // We have this check if there are already UnpositionedFloats as we aren't |
| // allowed to position a float "above" another float which has come before us |
| // in the document. |
| void NGLineBreaker::HandleFloat(const NGInlineItem& item, |
| NGLineInfo* line_info, |
| NGInlineItemResult* item_result) { |
| // When rewind occurs, an item may be handled multiple times. |
| // Since floats are put into a separate list, avoid handling same floats |
| // twice. |
| // Ideally rewind can take floats out of floats list, but the difference is |
| // sutble compared to the complexity. |
| // |
| // Additionally, we need to skip floats if we're retrying a line after a |
| // fragmentainer break. In that case the floats associated with this line will |
| // already have been processed. |
| ComputeCanBreakAfter(item_result); |
| MoveToNextOf(item); |
| if (item_index_ <= handled_floats_end_item_index_ || ignore_floats_) |
| return; |
| |
| // Floats need to know the current line width to determine whether to put it |
| // into the current line or to the next line. Remove trailing spaces if this |
| // float is trailing, because whitespace should be collapsed across floats, |
| // and this logic requires the width after trailing spaces are collapsed. |
| if (IsTrailing(item, *line_info)) |
| RemoveTrailingCollapsibleSpace(line_info); |
| |
| NGBlockNode node(ToLayoutBox(item.GetLayoutObject())); |
| |
| const ComputedStyle& float_style = node.Style(); |
| NGBoxStrut margins = |
| ComputeMarginsForContainer(constraint_space_, float_style); |
| |
| // TODO(ikilpatrick): Add support for float break tokens inside an inline |
| // layout context. |
| scoped_refptr<NGUnpositionedFloat> unpositioned_float = |
| NGUnpositionedFloat::Create(constraint_space_.AvailableSize(), |
| constraint_space_.PercentageResolutionSize(), |
| constraint_space_.BfcOffset().line_offset, |
| constraint_space_.BfcOffset().line_offset, |
| margins, node, |
| /* break_token */ nullptr); |
| |
| LayoutUnit inline_margin_size = |
| (ComputeInlineSizeForUnpositionedFloat(constraint_space_, |
| unpositioned_float.get()) + |
| margins.InlineSum()) |
| .ClampNegativeToZero(); |
| |
| LayoutUnit bfc_block_offset = line_.line_opportunity.bfc_block_offset; |
| |
| // The float should be positioned after the current line if: |
| // - It can't fit. |
| // - It will be moved down due to block-start edge alignment. |
| // - It will be moved down due to clearance. |
| // - We are currently computing our min/max-content size. (We use the |
| // unpositioned_floats to manually adjust the min/max-content size after |
| // the line breaker has run). |
| bool float_after_line = |
| !line_.CanFit(inline_margin_size) || |
| exclusion_space_->LastFloatBlockStart() > bfc_block_offset || |
| exclusion_space_->ClearanceOffset(float_style.Clear()) > |
| bfc_block_offset || |
| mode_ != NGLineBreakerMode::kContent; |
| |
| // Check if we already have a pending float. That's because a float cannot be |
| // higher than any block or floated box generated before. |
| if (!unpositioned_floats_->IsEmpty() || float_after_line) { |
| AddUnpositionedFloat(unpositioned_floats_, container_builder_, |
| std::move(unpositioned_float)); |
| } else { |
| NGPositionedFloat positioned_float = PositionFloat( |
| bfc_block_offset, constraint_space_.BfcOffset().block_offset, |
| unpositioned_float.get(), constraint_space_, exclusion_space_); |
| positioned_floats_->push_back(positioned_float); |
| |
| DCHECK_EQ(positioned_float.bfc_offset.block_offset, |
| bfc_block_offset + margins.block_start); |
| |
| if (float_style.Floating() == EFloat::kLeft) { |
| line_.line_opportunity.line_left_offset = std::max( |
| line_.line_opportunity.line_left_offset, |
| positioned_float.bfc_offset.line_offset + inline_margin_size - |
| margins.LineLeft(TextDirection::kLtr)); |
| } else { |
| line_.line_opportunity.line_right_offset = |
| std::min(line_.line_opportunity.line_right_offset, |
| positioned_float.bfc_offset.line_offset - |
| margins.LineLeft(TextDirection::kLtr)); |
| } |
| |
| DCHECK_GE(line_.AvailableWidth(), LayoutUnit()); |
| } |
| } |
| |
| bool NGLineBreaker::ComputeOpenTagResult( |
| const NGInlineItem& item, |
| const NGConstraintSpace& constraint_space, |
| NGInlineItemResult* item_result) { |
| DCHECK_EQ(item.Type(), NGInlineItem::kOpenTag); |
| DCHECK(item.Style()); |
| const ComputedStyle& style = *item.Style(); |
| item_result->has_edge = item.HasStartEdge(); |
| if (item.ShouldCreateBoxFragment() && |
| (style.HasBorder() || style.HasPadding() || |
| (style.HasMargin() && item_result->has_edge))) { |
| NGBoxStrut borders = ComputeBorders(constraint_space, style); |
| NGBoxStrut paddings = ComputePadding(constraint_space, style); |
| item_result->padding = paddings; |
| item_result->borders_paddings_block_start = |
| borders.block_start + paddings.block_start; |
| item_result->borders_paddings_block_end = |
| borders.block_end + paddings.block_end; |
| if (item_result->has_edge) { |
| item_result->margins = ComputeMarginsForSelf(constraint_space, style); |
| item_result->inline_size = item_result->margins.inline_start + |
| borders.inline_start + paddings.inline_start; |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void NGLineBreaker::HandleOpenTag(const NGInlineItem& item, |
| NGInlineItemResult* item_result) { |
| DCHECK(!item_result->can_break_after); |
| |
| if (ComputeOpenTagResult(item, constraint_space_, item_result)) { |
| line_.position += item_result->inline_size; |
| |
| // While the spec defines "non-zero margins, padding, or borders" prevents |
| // line boxes to be zero-height, tests indicate that only inline direction |
| // of them do so. See should_create_line_box_. |
| // Force to create a box, because such inline boxes affect line heights. |
| if (!line_.should_create_line_box && |
| (item_result->inline_size || |
| (item_result->margins.inline_start && !in_line_height_quirks_mode_))) |
| line_.should_create_line_box = true; |
| } |
| |
| DCHECK(item.Style()); |
| const ComputedStyle& style = *item.Style(); |
| SetCurrentStyle(style); |
| MoveToNextOf(item); |
| } |
| |
| void NGLineBreaker::HandleCloseTag(const NGInlineItem& item, |
| NGInlineItemResults* item_results) { |
| NGInlineItemResult* item_result = AddItem(item, item_results); |
| item_result->has_edge = item.HasEndEdge(); |
| if (item_result->has_edge) { |
| DCHECK(item.Style()); |
| const ComputedStyle& style = *item.Style(); |
| item_result->margins = ComputeMarginsForSelf(constraint_space_, style); |
| NGBoxStrut borders = ComputeBorders(constraint_space_, style); |
| NGBoxStrut paddings = ComputePadding(constraint_space_, style); |
| item_result->inline_size = item_result->margins.inline_end + |
| borders.inline_end + paddings.inline_end; |
| line_.position += item_result->inline_size; |
| |
| if (!line_.should_create_line_box && |
| (item_result->inline_size || |
| (item_result->margins.inline_end && !in_line_height_quirks_mode_))) |
| line_.should_create_line_box = true; |
| } |
| DCHECK(item.GetLayoutObject() && item.GetLayoutObject()->Parent()); |
| bool was_auto_wrap = auto_wrap_; |
| SetCurrentStyle(item.GetLayoutObject()->Parent()->StyleRef()); |
| MoveToNextOf(item); |
| |
| // Prohibit break before a close tag by setting can_break_after to the |
| // previous result. |
| // TODO(kojii): There should be a result before close tag, but there are cases |
| // that doesn't because of the way we handle trailing spaces. This needs to be |
| // revisited. |
| if (item_results->size() >= 2) { |
| NGInlineItemResult* last = &(*item_results)[item_results->size() - 2]; |
| if (was_auto_wrap == auto_wrap_) { |
| item_result->can_break_after = last->can_break_after; |
| last->can_break_after = false; |
| return; |
| } |
| last->can_break_after = false; |
| if (!was_auto_wrap) { |
| DCHECK(auto_wrap_); |
| // When auto-wrap starts after no-wrap, the boundary is not allowed to |
| // wrap. However, when space characters follow the boundary, there should |
| // be a break opportunity after the space. The break_iterator cannot |
| // compute this because it considers break opportunities are before a run |
| // of spaces. |
| const String& text = Text(); |
| if (offset_ < text.length() && IsBreakableSpace(text[offset_])) { |
| item_result->can_break_after = true; |
| return; |
| } |
| } |
| } |
| ComputeCanBreakAfter(item_result); |
| } |
| |
| // 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(). |
| NGLineBreaker::LineBreakState NGLineBreaker::HandleOverflow( |
| NGLineInfo* line_info) { |
| return HandleOverflow(line_info, line_.AvailableWidth()); |
| } |
| |
| NGLineBreaker::LineBreakState NGLineBreaker::HandleOverflow( |
| NGLineInfo* line_info, |
| LayoutUnit available_width) { |
| NGInlineItemResults* item_results = &line_info->Results(); |
| LayoutUnit width_to_rewind = line_.position - available_width; |
| DCHECK_GT(width_to_rewind, 0); |
| |
| // Keep track of the shortest break opportunity. |
| unsigned break_before = 0; |
| |
| // Search for a break opportunity that can fit. |
| for (unsigned i = item_results->size(); i;) { |
| NGInlineItemResult* item_result = &(*item_results)[--i]; |
| |
| // Try to break after this item. |
| if (i < item_results->size() - 1 && item_result->can_break_after) { |
| if (width_to_rewind <= 0) { |
| line_.position = available_width + width_to_rewind; |
| Rewind(line_info, i + 1); |
| return LineBreakState::kTrailing; |
| } |
| break_before = i + 1; |
| } |
| |
| // Try to break inside of this item. |
| LayoutUnit next_width_to_rewind = |
| width_to_rewind - item_result->inline_size; |
| DCHECK(item_result->item); |
| const NGInlineItem& item = *item_result->item; |
| if (item.Type() == NGInlineItem::kText && next_width_to_rewind < 0 && |
| (item_result->may_break_inside || override_break_anywhere_)) { |
| // When the text fits but its right margin does not, the break point |
| // must not be at the end. |
| LayoutUnit item_available_width = |
| std::min(-next_width_to_rewind, item_result->inline_size - 1); |
| SetCurrentStyle(*item.Style()); |
| BreakText(item_result, item, item_available_width, line_info); |
| #if DCHECK_IS_ON() |
| item_result->CheckConsistency(); |
| #endif |
| if (item_result->inline_size <= item_available_width) { |
| DCHECK(item_result->end_offset < item.EndOffset()); |
| DCHECK(item_result->can_break_after); |
| DCHECK_LE(i + 1, item_results->size()); |
| if (i + 1 == item_results->size()) { |
| // If this is the last item, adjust states to accomodate the change. |
| line_.position = |
| available_width + next_width_to_rewind + item_result->inline_size; |
| if (line_info->LineEndFragment()) |
| SetLineEndFragment(nullptr, line_info); |
| #if DCHECK_IS_ON() |
| LayoutUnit position_fast = line_.position; |
| UpdatePosition(line_info->Results()); |
| DCHECK_EQ(line_.position, position_fast); |
| #endif |
| item_index_ = item_result->item_index; |
| offset_ = item_result->end_offset; |
| items_data_.AssertOffset(item_index_, offset_); |
| } else { |
| Rewind(line_info, i + 1); |
| } |
| return LineBreakState::kTrailing; |
| } |
| } |
| |
| width_to_rewind = next_width_to_rewind; |
| } |
| |
| // Reaching here means that the rewind point was not found. |
| |
| if (break_anywhere_if_overflow_ && !override_break_anywhere_) { |
| override_break_anywhere_ = true; |
| break_iterator_.SetBreakType(LineBreakType::kBreakCharacter); |
| Rewind(line_info, 0); |
| return LineBreakState::kContinue; |
| } |
| |
| // Let this line overflow. |
| // If there was a break opportunity, the overflow should stop there. |
| if (break_before) { |
| Rewind(line_info, break_before); |
| return LineBreakState::kTrailing; |
| } |
| |
| return LineBreakState::kTrailing; |
| } |
| |
| void NGLineBreaker::Rewind(NGLineInfo* line_info, unsigned new_end) { |
| NGInlineItemResults* item_results = &line_info->Results(); |
| DCHECK_LT(new_end, item_results->size()); |
| |
| // TODO(ikilpatrick): Add DCHECK that we never rewind past any floats. |
| |
| if (new_end) { |
| // Use |results[new_end - 1].end_offset| because it may have been truncated |
| // and may not be equal to |results[new_end].start_offset|. |
| MoveToNextOf((*item_results)[new_end - 1]); |
| } else { |
| // When rewinding all items, use |results[0].start_offset|. |
| const NGInlineItemResult& first_remove = (*item_results)[new_end]; |
| item_index_ = first_remove.item_index; |
| offset_ = first_remove.start_offset; |
| } |
| |
| // TODO(kojii): Should we keep results for the next line? We don't need to |
| // re-layout atomic inlines. |
| item_results->Shrink(new_end); |
| |
| SetLineEndFragment(nullptr, line_info); |
| UpdatePosition(line_info->Results()); |
| } |
| |
| // Returns the LayoutObject at the current index/offset. |
| // This is to tie generated fragments to the source DOM node/LayoutObject for |
| // paint invalidations, hit testing, etc. |
| LayoutObject* NGLineBreaker::CurrentLayoutObject( |
| const NGLineInfo& line_info) const { |
| const Vector<NGInlineItem>& items = line_info.ItemsData().items; |
| DCHECK_LE(item_index_, items.size()); |
| // Find the next item that has LayoutObject. Some items such as bidi controls |
| // do not have LayoutObject. |
| for (unsigned i = item_index_; i < items.size(); i++) { |
| if (LayoutObject* layout_object = items[i].GetLayoutObject()) |
| return layout_object; |
| } |
| // Find the last item if there were no LayoutObject afterwards. |
| for (unsigned i = item_index_; i--;) { |
| if (LayoutObject* layout_object = items[i].GetLayoutObject()) |
| return layout_object; |
| } |
| NOTREACHED(); |
| return nullptr; |
| } |
| |
| void NGLineBreaker::SetCurrentStyle(const ComputedStyle& style) { |
| current_style_ = &style; |
| |
| auto_wrap_ = style.AutoWrap(); |
| |
| if (auto_wrap_) { |
| break_iterator_.SetLocale(style.LocaleForLineBreakIterator()); |
| |
| if (UNLIKELY(override_break_anywhere_)) { |
| break_iterator_.SetBreakType(LineBreakType::kBreakCharacter); |
| } else { |
| switch (style.WordBreak()) { |
| case EWordBreak::kNormal: |
| break_anywhere_if_overflow_ = |
| style.OverflowWrap() == EOverflowWrap::kBreakWord; |
| break_iterator_.SetBreakType(LineBreakType::kNormal); |
| break; |
| case EWordBreak::kBreakAll: |
| break_anywhere_if_overflow_ = false; |
| break_iterator_.SetBreakType(LineBreakType::kBreakAll); |
| break; |
| case EWordBreak::kBreakWord: |
| break_anywhere_if_overflow_ = true; |
| break_iterator_.SetBreakType(LineBreakType::kNormal); |
| break; |
| case EWordBreak::kKeepAll: |
| break_anywhere_if_overflow_ = false; |
| break_iterator_.SetBreakType(LineBreakType::kKeepAll); |
| break; |
| } |
| } |
| |
| enable_soft_hyphen_ = style.GetHyphens() != Hyphens::kNone; |
| hyphenation_ = style.GetHyphenation(); |
| } |
| |
| spacing_.SetSpacing(style.GetFontDescription()); |
| } |
| |
| void NGLineBreaker::MoveToNextOf(const NGInlineItem& item) { |
| offset_ = item.EndOffset(); |
| item_index_++; |
| } |
| |
| void NGLineBreaker::MoveToNextOf(const NGInlineItemResult& item_result) { |
| offset_ = item_result.end_offset; |
| item_index_ = item_result.item_index; |
| DCHECK(item_result.item); |
| if (offset_ == item_result.item->EndOffset()) |
| item_index_++; |
| } |
| |
| scoped_refptr<NGInlineBreakToken> NGLineBreaker::CreateBreakToken( |
| const NGLineInfo& line_info, |
| std::unique_ptr<const NGInlineLayoutStateStack> state_stack) const { |
| const Vector<NGInlineItem>& items = Items(); |
| if (item_index_ >= items.size()) |
| return NGInlineBreakToken::Create(node_); |
| return NGInlineBreakToken::Create( |
| node_, current_style_.get(), item_index_, offset_, |
| ((line_.is_after_forced_break ? NGInlineBreakToken::kIsForcedBreak : 0) | |
| (line_info.UseFirstLineStyle() ? NGInlineBreakToken::kUseFirstLineStyle |
| : 0)), |
| std::move(state_stack)); |
| } |
| |
| } // namespace blink |