blob: e659d9745459a72f39c7beb4fb681bcd20e20567 [file] [log] [blame]
// 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