diff options
Diffstat (limited to 'chromium/third_party/blink/renderer/core/layout/ng/inline')
40 files changed, 1982 insertions, 931 deletions
diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.cc new file mode 100644 index 00000000000..614d0403810 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.cc @@ -0,0 +1,224 @@ +// Copyright 2018 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_abstract_inline_text_box.h" + +#include "third_party/blink/renderer/core/accessibility/ax_object_cache.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h" +#include "third_party/blink/renderer/core/paint/ng/ng_paint_fragment.h" +#include "third_party/blink/renderer/core/paint/ng/ng_paint_fragment_traversal.h" +#include "third_party/blink/renderer/platform/fonts/character_range.h" +#include "third_party/blink/renderer/platform/fonts/shaping/shape_result_buffer.h" + +namespace blink { + +NGAbstractInlineTextBox::FragmentToNGAbstractInlineTextBoxHashMap* + NGAbstractInlineTextBox::g_abstract_inline_text_box_map_ = nullptr; + +scoped_refptr<AbstractInlineTextBox> NGAbstractInlineTextBox::GetOrCreate( + LineLayoutText line_layout_item, + const NGPaintFragment& fragment) { + DCHECK(fragment.GetLayoutObject()->IsText()) << fragment.GetLayoutObject(); + if (!g_abstract_inline_text_box_map_) { + g_abstract_inline_text_box_map_ = + new FragmentToNGAbstractInlineTextBoxHashMap(); + } + const auto it = g_abstract_inline_text_box_map_->find(&fragment); + if (it != g_abstract_inline_text_box_map_->end()) + return it->value; + scoped_refptr<AbstractInlineTextBox> obj = + base::AdoptRef(new NGAbstractInlineTextBox(line_layout_item, fragment)); + g_abstract_inline_text_box_map_->Set(&fragment, obj); + return obj; +} + +void NGAbstractInlineTextBox::WillDestroy(NGPaintFragment* fragment) { + if (!g_abstract_inline_text_box_map_) + return; + const auto it = g_abstract_inline_text_box_map_->find(fragment); + if (it != g_abstract_inline_text_box_map_->end()) { + it->value->Detach(); + g_abstract_inline_text_box_map_->erase(fragment); + } +} + +NGAbstractInlineTextBox::NGAbstractInlineTextBox( + LineLayoutText line_layout_item, + const NGPaintFragment& fragment) + : AbstractInlineTextBox(line_layout_item), fragment_(&fragment) { + DCHECK(fragment_->PhysicalFragment().IsText()) << fragment_; +} + +NGAbstractInlineTextBox::~NGAbstractInlineTextBox() { + DCHECK(!fragment_); +} + +void NGAbstractInlineTextBox::Detach() { + if (Node* const node = GetNode()) { + if (AXObjectCache* cache = node->GetDocument().ExistingAXObjectCache()) + cache->InlineTextBoxesUpdated(GetLineLayoutItem()); + } + AbstractInlineTextBox::Detach(); + fragment_ = nullptr; +} + +bool NGAbstractInlineTextBox::HasSoftWrapToNextLine() const { + return ToNGPhysicalLineBoxFragment( + fragment_->ContainerLineBox()->PhysicalFragment()) + .HasSoftWrapToNextLine(); +} + +const NGPhysicalTextFragment& NGAbstractInlineTextBox::PhysicalTextFragment() + const { + return ToNGPhysicalTextFragment(fragment_->PhysicalFragment()); +} + +bool NGAbstractInlineTextBox::NeedsLayout() const { + return fragment_->GetLayoutObject()->NeedsLayout(); +} + +bool NGAbstractInlineTextBox::NeedsTrailingSpace() const { + if (!HasSoftWrapToNextLine()) + return false; + const NGPaintFragment* next_fragment = NextTextFragmentForSameLayoutObject(); + if (!next_fragment) + return false; + return ToNGPhysicalTextFragment(next_fragment->PhysicalFragment()) + .StartOffset() != PhysicalTextFragment().EndOffset(); +} + +const NGPaintFragment* +NGAbstractInlineTextBox::NextTextFragmentForSameLayoutObject() const { + const auto fragments = + NGPaintFragment::InlineFragmentsFor(fragment_->GetLayoutObject()); + const auto it = + std::find_if(fragments.begin(), fragments.end(), + [&](const auto& sibling) { return fragment_ == sibling; }); + DCHECK(it != fragments.end()); + const auto next_it = std::next(it); + return next_it == fragments.end() ? nullptr : *next_it; +} + +scoped_refptr<AbstractInlineTextBox> +NGAbstractInlineTextBox::NextInlineTextBox() const { + if (!fragment_) + return nullptr; + DCHECK(!NeedsLayout()); + const NGPaintFragment* next_fragment = NextTextFragmentForSameLayoutObject(); + if (!next_fragment) + return nullptr; + return GetOrCreate(GetLineLayoutItem(), *next_fragment); +} + +LayoutRect NGAbstractInlineTextBox::LocalBounds() const { + if (!fragment_ || !GetLineLayoutItem()) + return LayoutRect(); + return LayoutRect(fragment_->InlineOffsetToContainerBox().ToLayoutPoint(), + fragment_->Size().ToLayoutSize()); +} + +unsigned NGAbstractInlineTextBox::Len() const { + if (!fragment_) + return 0; + if (NeedsTrailingSpace()) + return PhysicalTextFragment().Length() + 1; + return PhysicalTextFragment().Length(); +} + +AbstractInlineTextBox::Direction NGAbstractInlineTextBox::GetDirection() const { + if (!fragment_ || !GetLineLayoutItem()) + return kLeftToRight; + const TextDirection text_direction = + PhysicalTextFragment().ResolvedDirection(); + if (GetLineLayoutItem().Style()->IsHorizontalWritingMode()) + return IsLtr(text_direction) ? kLeftToRight : kRightToLeft; + return IsLtr(text_direction) ? kTopToBottom : kBottomToTop; +} + +void NGAbstractInlineTextBox::CharacterWidths(Vector<float>& widths) const { + if (!fragment_) + return; + if (!PhysicalTextFragment().TextShapeResult()) { + // When |fragment_| for BR, we don't have shape result. + // "aom-computed-boolean-properties.html" reaches here. + widths.resize(Len()); + return; + } + const ShapeResult& shape_result = *PhysicalTextFragment().TextShapeResult(); + ShapeResultBuffer buffer; + buffer.AppendResult(&shape_result); + const Vector<CharacterRange> ranges = buffer.IndividualCharacterRanges( + shape_result.Direction(), shape_result.Width()); + widths.ReserveCapacity(ranges.size()); + widths.resize(0); + for (const auto& range : ranges) + widths.push_back(range.Width()); + // The shaper can fail to return glyph metrics for all characters (see + // crbug.com/613915 and crbug.com/615661) so add empty ranges to ensure all + // characters have an associated range. + widths.resize(Len()); +} + +String NGAbstractInlineTextBox::GetText() const { + if (!fragment_ || !GetLineLayoutItem()) + return String(); + // For compatibility with |InlineTextBox|, we should have a space character + // for soft line break. + // Following tests require this: + // - accessibility/inline-text-change-style.html + // - accessibility/inline-text-changes.html + // - accessibility/inline-text-word-boundaries.html + if (NeedsTrailingSpace()) + return PhysicalTextFragment().Text().ToString() + " "; + return PhysicalTextFragment().Text().ToString(); +} + +bool NGAbstractInlineTextBox::IsFirst() const { + if (!fragment_) + return true; + DCHECK(!NeedsLayout()); + const auto fragments = + NGPaintFragment::InlineFragmentsFor(fragment_->GetLayoutObject()); + return fragment_ == &fragments.front(); +} + +bool NGAbstractInlineTextBox::IsLast() const { + if (!fragment_) + return true; + DCHECK(!NeedsLayout()); + const auto fragments = + NGPaintFragment::InlineFragmentsFor(fragment_->GetLayoutObject()); + return fragment_ == &fragments.back(); +} + +scoped_refptr<AbstractInlineTextBox> NGAbstractInlineTextBox::NextOnLine() + const { + if (!fragment_) + return nullptr; + DCHECK(!NeedsLayout()); + DCHECK(fragment_->ContainerLineBox()); + NGPaintFragmentTraversal cursor(*fragment_->ContainerLineBox(), *fragment_); + for (cursor.MoveToNext(); !cursor.IsAtEnd(); cursor.MoveToNext()) { + if (cursor->GetLayoutObject()->IsText()) + return GetOrCreate(GetLineLayoutItem(), *cursor); + } + return nullptr; +} + +scoped_refptr<AbstractInlineTextBox> NGAbstractInlineTextBox::PreviousOnLine() + const { + if (!fragment_) + return nullptr; + DCHECK(!NeedsLayout()); + DCHECK(fragment_->ContainerLineBox()); + NGPaintFragmentTraversal cursor(*fragment_->ContainerLineBox(), *fragment_); + for (cursor.MoveToPrevious(); !cursor.IsAtEnd(); cursor.MoveToPrevious()) { + if (cursor->GetLayoutObject()->IsText()) + return GetOrCreate(GetLineLayoutItem(), *cursor); + } + return nullptr; +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.h new file mode 100644 index 00000000000..4932dec9d7d --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.h @@ -0,0 +1,68 @@ +// Copyright 2018 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. + +#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_ABSTRACT_INLINE_TEXT_BOX_H_ +#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_ABSTRACT_INLINE_TEXT_BOX_H_ + +#include "third_party/blink/renderer/core/layout/line/abstract_inline_text_box.h" + +namespace blink { + +class NGPaintFragment; +class NGPhysicalTextFragment; + +// The implementation of |AbstractInlineTextBox| for LayoutNG. +// See also |LegacyAbstractInlineTextBox| for legacy layout. +class CORE_EXPORT NGAbstractInlineTextBox final : public AbstractInlineTextBox { + private: + // Returns existing or newly created |NGAbstractInlineTextBox|. + // * |line_layout_item| is |LayoutText| associated to |fragment|. For first + // letter part, it is remaining part of |LayoutTextFragment|. + // * |fragment| should be attached to |NGPhysicalTextFragment|. + static scoped_refptr<AbstractInlineTextBox> GetOrCreate( + LineLayoutText line_layout_item, + const NGPaintFragment& fragment); + static void WillDestroy(NGPaintFragment*); + + friend class LayoutText; + friend class NGPaintFragment; + + public: + ~NGAbstractInlineTextBox() final; + + private: + NGAbstractInlineTextBox(LineLayoutText line_layout_item, + const NGPaintFragment& fragment); + + bool HasSoftWrapToNextLine() const; + const NGPhysicalTextFragment& PhysicalTextFragment() const; + bool NeedsLayout() const; + bool NeedsTrailingSpace() const; + // Returns next fragment associated to |LayoutText|. + const NGPaintFragment* NextTextFragmentForSameLayoutObject() const; + + // Implementations of AbstractInlineTextBox member functions. + void Detach() final; + scoped_refptr<AbstractInlineTextBox> NextInlineTextBox() const final; + LayoutRect LocalBounds() const final; + unsigned Len() const final; + Direction GetDirection() const final; + void CharacterWidths(Vector<float>&) const final; + String GetText() const final; + bool IsFirst() const final; + bool IsLast() const final; + scoped_refptr<AbstractInlineTextBox> NextOnLine() const final; + scoped_refptr<AbstractInlineTextBox> PreviousOnLine() const final; + + const NGPaintFragment* fragment_; + + using FragmentToNGAbstractInlineTextBoxHashMap = + HashMap<const NGPaintFragment*, scoped_refptr<AbstractInlineTextBox>>; + static FragmentToNGAbstractInlineTextBoxHashMap* + g_abstract_inline_text_box_map_; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_ABSTRACT_INLINE_TEXT_BOX_H_ diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_baseline.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_baseline.cc index e43c7bcd284..a65589efaea 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_baseline.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_baseline.cc @@ -23,7 +23,7 @@ bool NGBaseline::ShouldPropagateBaselines(const NGLayoutInputNode node) { if (node.IsInline()) return true; - return ShouldPropagateBaselines(ToLayoutBox(node.GetLayoutObject())); + return ShouldPropagateBaselines(node.GetLayoutBox()); } bool NGBaseline::ShouldPropagateBaselines(LayoutBox* layout_box) { @@ -39,12 +39,6 @@ bool NGBaseline::ShouldPropagateBaselines(LayoutBox* layout_box) { if (!NGBlockNode(layout_box).CanUseNewLayout()) return true; - // CSS defines certain cases to synthesize baselines from box. See comments in - // UseLogicalBottomMarginEdgeForInlineBlockBaseline(). - const LayoutBlock* layout_block = ToLayoutBlock(layout_box); - if (layout_block->UseLogicalBottomMarginEdgeForInlineBlockBaseline()) - return false; - return true; } diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.cc index cadd9c38992..a139ca08482 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.cc @@ -160,7 +160,8 @@ CaretPositionResolution TryResolveCaretPositionByBoxFragmentSide( const NGPaintFragment& fragment, unsigned offset, TextAffinity affinity) { - if (!fragment.GetNode()) { + // There is no caret position at a pseudo or generated box side. + if (!fragment.GetNode() || fragment.GetNode()->IsPseudoElement()) { // TODO(xiaochengh): This leads to false negatives for, e.g., RUBY, where an // anonymous wrapping inline block is created. return CaretPositionResolution(); @@ -289,25 +290,40 @@ NGCaretPosition ComputeNGCaretPosition(const PositionWithAffinity& position) { } Position NGCaretPosition::ToPositionInDOMTree() const { + return ToPositionInDOMTreeWithAffinity().GetPosition(); +} + +PositionWithAffinity NGCaretPosition::ToPositionInDOMTreeWithAffinity() const { if (!fragment) - return Position(); + return PositionWithAffinity(); switch (position_type) { case NGCaretPositionType::kBeforeBox: if (!fragment->GetNode()) - return Position(); - return Position::BeforeNode(*fragment->GetNode()); + return PositionWithAffinity(); + return PositionWithAffinity(Position::BeforeNode(*fragment->GetNode()), + TextAffinity::kDownstream); case NGCaretPositionType::kAfterBox: if (!fragment->GetNode()) - return Position(); - return Position::AfterNode(*fragment->GetNode()); + return PositionWithAffinity(); + return PositionWithAffinity(Position::AfterNode(*fragment->GetNode()), + TextAffinity::kUpstreamIfPossible); case NGCaretPositionType::kAtTextOffset: DCHECK(text_offset.has_value()); const NGOffsetMapping* mapping = NGOffsetMapping::GetFor(fragment->GetLayoutObject()); - return mapping->GetFirstPosition(*text_offset); + const Position position = mapping->GetFirstPosition(*text_offset); + if (position.IsNull()) + return PositionWithAffinity(); + const NGPhysicalTextFragment& text_fragment = + ToNGPhysicalTextFragment(fragment->PhysicalFragment()); + const TextAffinity affinity = + text_offset.value() == text_fragment.EndOffset() + ? TextAffinity::kUpstreamIfPossible + : TextAffinity::kDownstream; + return PositionWithAffinity(position, affinity); } NOTREACHED(); - return Position(); + return PositionWithAffinity(); } } // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.h index e6a963099c6..cb3beefdfe1 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.h @@ -32,6 +32,7 @@ struct NGCaretPosition { bool IsNull() const { return !fragment; } Position ToPositionInDOMTree() const; + PositionWithAffinity ToPositionInDOMTreeWithAffinity() const; const NGPaintFragment* fragment = nullptr; // owned by root LayoutNGMixin NGCaretPositionType position_type; diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position_test.cc index da232c2bf83..6446ddb34de 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position_test.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position_test.cc @@ -276,4 +276,23 @@ TEST_F(NGCaretPositionTest, CaretPositionAtSoftLineWrapBetweenDeepTextNodes) { fragment_d, kAtTextOffset, base::Optional<unsigned>(wrap_offset)); } +TEST_F(NGCaretPositionTest, InlineBlockBeforeContent) { + SetInlineFormattingContext( + "t", + "<style>span::before{display:inline-block; content:'foo'}</style>" + "<span id=span>bar</span>", + 100); // Line width doesn't matter here. + const Node* text = GetElementById("span")->firstChild(); + const NGPhysicalFragment* text_fragment = FragmentOf(text); + + // Test caret position of "|bar", which shouldn't be affected by ::before + const Position position(text, 0); + const NGOffsetMapping& mapping = *NGOffsetMapping::GetFor(position); + const unsigned text_offset = mapping.GetTextContentOffset(position).value(); + + TEST_CARET(ComputeNGCaretPosition(text_offset, TextAffinity::kDownstream), + text_fragment, kAtTextOffset, + base::Optional<unsigned>(text_offset)); +} + } // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.cc index fca16e8c347..97202bc6428 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.cc @@ -15,12 +15,41 @@ namespace blink { +namespace { + +NGLineHeightMetrics ComputeEmphasisMarkOutsets(const ComputedStyle& style) { + if (style.GetTextEmphasisMark() == TextEmphasisMark::kNone) + return NGLineHeightMetrics(); + + const Font& font = style.GetFont(); + LayoutUnit emphasis_mark_height = + LayoutUnit(font.EmphasisMarkHeight(style.TextEmphasisMarkString())); + DCHECK_GT(emphasis_mark_height, LayoutUnit()); + return style.GetTextEmphasisLineLogicalSide() == LineLogicalSide::kOver + ? NGLineHeightMetrics(emphasis_mark_height, LayoutUnit()) + : NGLineHeightMetrics(LayoutUnit(), emphasis_mark_height); +} + +} // namespace + void NGInlineBoxState::ComputeTextMetrics(const ComputedStyle& style, FontBaseline baseline_type) { text_metrics = NGLineHeightMetrics(style, baseline_type); text_top = -text_metrics.ascent; text_height = text_metrics.LineHeight(); - text_metrics.AddLeading(style.ComputedLineHeightAsFixed()); + + NGLineHeightMetrics emphasis_marks_outsets = + ComputeEmphasisMarkOutsets(style); + if (emphasis_marks_outsets.IsEmpty()) { + text_metrics.AddLeading(style.ComputedLineHeightAsFixed()); + } else { + NGLineHeightMetrics emphasis_marks_metrics = text_metrics; + emphasis_marks_metrics += emphasis_marks_outsets; + text_metrics.AddLeading(style.ComputedLineHeightAsFixed()); + text_metrics.Unite(emphasis_marks_metrics); + // TODO: Is this correct to include into text_metrics? How do we use + // text_metrics after this point? + } metrics.Unite(text_metrics); @@ -37,7 +66,7 @@ void NGInlineBoxState::AccumulateUsedFonts(const ShapeResult* shape_result, FontBaseline baseline_type) { HashSet<const SimpleFontData*> fallback_fonts; shape_result->FallbackFonts(&fallback_fonts); - for (auto* const fallback_font : fallback_fonts) { + for (const SimpleFontData* const fallback_font : fallback_fonts) { NGLineHeightMetrics fallback_metrics(fallback_font->GetFontMetrics(), baseline_type); fallback_metrics.AddLeading( @@ -61,7 +90,7 @@ LayoutObject* NGInlineLayoutStateStack::ContainingLayoutObjectForAbsolutePositionObjects() const { for (unsigned i = stack_.size(); i-- > 1;) { - const auto& box = stack_[i]; + const NGInlineBoxState& box = stack_[i]; DCHECK(box.style); if (box.style->CanContainAbsolutePositionObjects()) { DCHECK(box.item->GetLayoutObject()); @@ -82,7 +111,7 @@ NGInlineBoxState* NGInlineLayoutStateStack::OnBeginPlaceItems( box->fragment_start = 0; } else { // For the following lines, clear states that are not shared across lines. - for (auto& box : stack_) { + for (NGInlineBoxState& box : stack_) { box.fragment_start = 0; if (!line_height_quirk) box.metrics = box.text_metrics; @@ -136,8 +165,8 @@ NGInlineBoxState* NGInlineLayoutStateStack::OnOpenTag( DCHECK_EQ(item_result.margins.inline_start, LayoutUnit()); DCHECK_EQ(item_result.inline_size, LayoutUnit()); } - box->border_padding_block_start = item_result.borders_paddings_block_start; - box->border_padding_block_end = item_result.borders_paddings_block_end; + box->border_padding_line_over = item_result.borders_paddings_line_over; + box->border_padding_line_under = item_result.borders_paddings_line_under; return box; } @@ -182,10 +211,21 @@ void NGInlineLayoutStateStack::EndBoxState( PositionPending position_pending = ApplyBaselineShift(box, line_box, baseline_type); + // We are done here if there is no parent box. + if (box == stack_.begin()) + return; + NGInlineBoxState& parent_box = *std::prev(box); + + // Propagate necessary data back to the parent box. + // Unite the metrics to the parent box. - if (position_pending == kPositionNotPending && box != stack_.begin()) { - box[-1].metrics.Unite(box->metrics); - } + if (position_pending == kPositionNotPending) + parent_box.metrics.Unite(box->metrics); + + // Create box fragments for parent if the current box has properties (e.g., + // margin) that make it tricky to compute the parent's rects. + if (box->ParentNeedsBoxFragment(parent_box)) + parent_box.SetNeedsBoxFragment(); } void NGInlineBoxState::SetNeedsBoxFragment() { @@ -208,6 +248,23 @@ void NGInlineBoxState::SetLineRightForBoxFragment( } } +bool NGInlineBoxState::ParentNeedsBoxFragment( + const NGInlineBoxState& parent) const { + if (!parent.item) + return false; + // Below are the known cases where parent rect may not equal the union of + // its child rects. + if (margin_inline_start || margin_inline_end) + return true; + // Inline box height is determined by font metrics, which can be different + // from the height of its child atomic inline. + if (item && item->Type() == NGInlineItem::kAtomicInline) + return true; + // Returns true when parent and child boxes have different font metrics, since + // they may have different heights and/or locations in block direction. + return text_metrics != parent.text_metrics; +} + // Crete a placeholder for a box fragment. // We keep a flat list of fragments because it is more suitable for operations // such as ApplyBaselineShift. Later, CreateBoxFragments() creates box fragments @@ -228,10 +285,10 @@ void NGInlineLayoutStateStack::AddBoxFragmentPlaceholder( // Extend the block direction of the box by borders and paddings. Inline // direction is already included into positions in NGLineBreaker. NGLogicalOffset offset(LayoutUnit(), - -metrics.ascent - box->border_padding_block_start); + -metrics.ascent - box->border_padding_line_over); NGLogicalSize size(LayoutUnit(), metrics.LineHeight() + - box->border_padding_block_start + - box->border_padding_block_end); + box->border_padding_line_over + + box->border_padding_line_under); unsigned fragment_end = line_box->size(); DCHECK(box->item); @@ -289,7 +346,7 @@ void NGInlineLayoutStateStack::PrepareForReorder( NGLineBoxFragmentBuilder::ChildList* line_box) { // Set indexes of BoxData to the children of the line box. unsigned box_data_index = 0; - for (const auto& box_data : box_data_list_) { + for (const BoxData& box_data : box_data_list_) { box_data_index++; for (unsigned i = box_data.fragment_start; i < box_data.fragment_end; i++) { NGLineBoxFragmentBuilder::Child& child = (*line_box)[i]; @@ -300,7 +357,7 @@ void NGInlineLayoutStateStack::PrepareForReorder( // When boxes are nested, placeholders have indexes to which box it should be // added. Copy them to BoxData. - for (auto& box_data : box_data_list_) { + for (BoxData& box_data : box_data_list_) { const NGLineBoxFragmentBuilder::Child& placeholder = (*line_box)[box_data.fragment_end]; DCHECK(!placeholder.HasFragment()); @@ -312,11 +369,11 @@ void NGInlineLayoutStateStack::PrepareForReorder( void NGInlineLayoutStateStack::UpdateAfterReorder( NGLineBoxFragmentBuilder::ChildList* line_box) { // Compute start/end of boxes from the children of the line box. - for (auto& box_data : box_data_list_) + for (BoxData& box_data : box_data_list_) box_data.fragment_start = box_data.fragment_end = 0; for (unsigned i = 0; i < line_box->size(); i++) { - const auto& child = (*line_box)[i]; - if (!child.HasFragment()) + const NGLineBoxFragmentBuilder::Child& child = (*line_box)[i]; + if (child.IsPlaceholder()) continue; if (unsigned box_data_index = child.box_data_index) { BoxData& box_data = box_data_list_[box_data_index - 1]; @@ -327,7 +384,7 @@ void NGInlineLayoutStateStack::UpdateAfterReorder( } // Extend start/end of boxes when they are nested. - for (auto& box_data : box_data_list_) { + for (BoxData& box_data : box_data_list_) { if (box_data.box_data_index) { BoxData& parent_box_data = box_data_list_[box_data.box_data_index - 1]; if (!parent_box_data.fragment_end) { @@ -344,7 +401,7 @@ void NGInlineLayoutStateStack::UpdateAfterReorder( #if DCHECK_IS_ON() // Check all BoxData have ranges. - for (const auto& box_data : box_data_list_) { + for (const BoxData& box_data : box_data_list_) { DCHECK_NE(box_data.fragment_end, 0u); DCHECK_GT(box_data.fragment_end, box_data.fragment_start); } @@ -356,7 +413,7 @@ LayoutUnit NGInlineLayoutStateStack::ComputeInlinePositions( // At this point, children are in the visual order, and they have their // origins at (0, 0). Accumulate inline offset from left to right. LayoutUnit position; - for (auto& child : *line_box) { + for (NGLineBoxFragmentBuilder::Child& child : *line_box) { child.offset.inline_offset += position; // Box margins/boders/paddings will be processed later. // TODO(kojii): we could optimize this if the reordering did not occur. @@ -368,27 +425,12 @@ LayoutUnit NGInlineLayoutStateStack::ComputeInlinePositions( if (box_data_list_.IsEmpty()) return position; - // Compute inline positions of inline boxes. - for (auto& box_data : box_data_list_) { + // Adjust child offsets for margin/border/padding of inline boxes. + for (BoxData& box_data : box_data_list_) { unsigned start = box_data.fragment_start; unsigned end = box_data.fragment_end; DCHECK_GT(end, start); - NGLineBoxFragmentBuilder::Child& start_child = (*line_box)[start]; - - // Clamping left offset is not defined, match to the existing behavior. - LayoutUnit line_left_offset = - start_child.offset.inline_offset.ClampNegativeToZero(); - LayoutUnit line_right_offset = end < line_box->size() - ? (*line_box)[end].offset.inline_offset - : position; - box_data.offset.inline_offset = - line_left_offset + box_data.margin_line_left; - box_data.size.inline_size = - line_right_offset - line_left_offset + - box_data.margin_border_padding_line_left - box_data.margin_line_left + - box_data.margin_border_padding_line_right - box_data.margin_line_right; - // Adjust child offsets for margin/border/padding. if (box_data.margin_border_padding_line_left) { line_box->MoveInInlineDirection(box_data.margin_border_padding_line_left, start, line_box->size()); @@ -402,6 +444,39 @@ LayoutUnit NGInlineLayoutStateStack::ComputeInlinePositions( } } + // Compute positions and sizes of inline boxes. + // + // Accumulate margin/border/padding of boxes for each child, to place nested + // parent boxes relative to the leaf (text or atomic inline) child. + struct LinePadding { + LayoutUnit line_left; + LayoutUnit line_right; + }; + Vector<LinePadding, 32> accumulated_padding(line_box->size()); + for (BoxData& box_data : box_data_list_) { + // Compute line-left and line-right edge of this box by accomodating + // border/padding of this box and margin/border/padding of descendants + // boxes, while accumulating its margin/border/padding. + unsigned start = box_data.fragment_start; + NGLineBoxFragmentBuilder::Child& start_child = (*line_box)[start]; + LayoutUnit line_left_offset = start_child.offset.inline_offset; + LinePadding& start_padding = accumulated_padding[start]; + start_padding.line_left += box_data.margin_border_padding_line_left; + line_left_offset -= start_padding.line_left - box_data.margin_line_left; + + DCHECK_GT(box_data.fragment_end, start); + unsigned last = box_data.fragment_end - 1; + NGLineBoxFragmentBuilder::Child& last_child = (*line_box)[last]; + LayoutUnit line_right_offset = + last_child.offset.inline_offset + last_child.inline_size; + LinePadding& last_padding = accumulated_padding[last]; + last_padding.line_right += box_data.margin_border_padding_line_right; + line_right_offset += last_padding.line_right - box_data.margin_line_right; + + box_data.offset.inline_offset = line_left_offset; + box_data.size.inline_size = line_right_offset - line_left_offset; + } + return position; } @@ -409,7 +484,7 @@ void NGInlineLayoutStateStack::CreateBoxFragments( NGLineBoxFragmentBuilder::ChildList* line_box) { DCHECK(!box_data_list_.IsEmpty()); - for (auto& box_data : box_data_list_) { + for (BoxData& box_data : box_data_list_) { unsigned start = box_data.fragment_start; unsigned end = box_data.fragment_end; DCHECK_GT(end, start); @@ -463,8 +538,8 @@ NGInlineLayoutStateStack::BoxData::CreateBoxFragment( // NGInlineLayoutAlgorithm can handle them later. DCHECK(!child.HasInFlowFragment()); } - - return box.ToBoxFragment(); + box.MoveOutOfFlowDescendantCandidatesToDescendants(); + return box.ToInlineBoxFragment(); } NGInlineLayoutStateStack::PositionPending @@ -477,7 +552,7 @@ NGInlineLayoutStateStack::ApplyBaselineShift( // |pending_descendants|. LayoutUnit baseline_shift; if (!box->pending_descendants.IsEmpty()) { - for (auto& child : box->pending_descendants) { + for (NGPendingPositions& child : box->pending_descendants) { if (child.metrics.IsEmpty()) { // This can happen with boxes with no content in quirks mode child.metrics = NGLineHeightMetrics(LayoutUnit(), LayoutUnit()); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.h index 626ad129d87..bfd5b61b6e5 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.h @@ -57,14 +57,14 @@ struct NGInlineBoxState { // is set. bool has_start_edge = false; bool has_end_edge = false; - NGBoxStrut padding; + NGLineBoxStrut padding; // |CreateBoxFragment()| needs margin, border+padding, and the sum of them. LayoutUnit margin_inline_start; LayoutUnit margin_inline_end; LayoutUnit margin_border_padding_inline_start; LayoutUnit margin_border_padding_inline_end; - LayoutUnit border_padding_block_start; - LayoutUnit border_padding_block_end; + LayoutUnit border_padding_line_over; + LayoutUnit border_padding_line_under; Vector<NGPendingPositions> pending_descendants; bool include_used_fonts = false; @@ -91,6 +91,12 @@ struct NGInlineBoxState { void SetLineRightForBoxFragment(const NGInlineItem&, const NGInlineItemResult&); + // In certain circumstances, the parent's rects is not a simple union of its + // children fragments' rects, e.g., when children have margin. In such cases, + // we should create box fragments for the parent to avoid hacky fixup when + // computing its rects. + bool ParentNeedsBoxFragment(const NGInlineBoxState& parent) const; + // Returns if the text style can be added without open-tag. // Text with different font or vertical-align needs to be wrapped with an // inline box. @@ -179,7 +185,7 @@ class CORE_EXPORT NGInlineLayoutStateStack { bool has_line_left_edge = false; bool has_line_right_edge = false; - NGBoxStrut padding; + NGLineBoxStrut padding; // |CreateBoxFragment()| needs margin, border+padding, and the sum of them. LayoutUnit margin_line_left; LayoutUnit margin_line_right; diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal.cc index 353acbabc68..6ce8dc88622 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal.cc @@ -14,70 +14,153 @@ namespace { using Result = NGPhysicalFragmentWithOffset; -// Traverse the subtree of |container|, and collect the fragments satisfying -// |filter| into the |results| vector. Guarantees to call |filter.AddOnEnter()| -// for all fragments in preorder, and call |filter.RemoveOnExit()| on all -// fragments in postorder. A fragment is collected if |AddOnEnter()| returns -// true and |RemoveOnExit()| returns false on it. -template <typename Filter, size_t inline_capacity> -void CollectInlineFragments(const NGPhysicalContainerFragment& container, - NGPhysicalOffset offset_to_container_box, - Filter& filter, - Vector<Result, inline_capacity>* results) { - DCHECK(container.IsInline() || container.IsLineBox() || - (container.IsBlockFlow() && - ToNGPhysicalBoxFragment(container).ChildrenInline())); - for (const auto& child : container.Children()) { - NGPhysicalOffset child_offset = child->Offset() + offset_to_container_box; - - if (filter.AddOnEnter(child.get())) { - results->push_back( - NGPhysicalFragmentWithOffset{child.get(), child_offset}); - } +class NGPhysicalFragmentCollectorBase { + STACK_ALLOCATED(); + + public: + virtual Vector<Result> CollectFrom(const NGPhysicalFragment&) = 0; + + protected: + explicit NGPhysicalFragmentCollectorBase() = default; + + virtual void Visit() = 0; + + const NGPhysicalFragment& GetFragment() const { return *current_fragment_; } + void SetShouldStopTraversing() { should_stop_traversing_ = true; } + bool HasStoppedTraversing() const { return should_stop_traversing_; } + + void Emit() { + results_.push_back(Result{current_fragment_, current_offset_to_root_}); + } + + // Visits and collets fragments in the subtree rooted at |fragment|. + // |fragment| itself is not visited. + Vector<Result> CollectExclusivelyFrom(const NGPhysicalFragment& fragment) { + current_fragment_ = &fragment; + root_fragment_ = &fragment; + VisitChildren(); + return std::move(results_); + } + + // Visits and collets fragments in the subtree rooted at |fragment|. + // |fragment| itself is visited. + Vector<Result> CollectInclusivelyFrom(const NGPhysicalFragment& fragment) { + current_fragment_ = &fragment; + root_fragment_ = &fragment; + Visit(); + return std::move(results_); + } + + void VisitChildren() { + if (should_stop_traversing_) + return; + + const NGPhysicalFragment& fragment = *current_fragment_; + if (!fragment.IsContainer()) + return; // Traverse descendants unless the fragment is laid out separately from the // inline layout algorithm. - if (child->IsContainer() && !child->IsBlockLayoutRoot()) { - CollectInlineFragments(ToNGPhysicalContainerFragment(*child), - child_offset, filter, results); - } + if (&fragment != root_fragment_ && fragment.IsBlockLayoutRoot()) + return; + + DCHECK(fragment.IsContainer()); + DCHECK(fragment.IsInline() || fragment.IsLineBox() || + (fragment.IsBlockFlow() && + ToNGPhysicalBoxFragment(fragment).ChildrenInline())); - if (filter.RemoveOnExit(child.get())) { - DCHECK(results->size()); - DCHECK_EQ(results->back().fragment, child.get()); - results->pop_back(); + for (const auto& child : + ToNGPhysicalContainerFragment(fragment).Children()) { + base::AutoReset<NGPhysicalOffset> offset_resetter( + ¤t_offset_to_root_, current_offset_to_root_ + child->Offset()); + base::AutoReset<const NGPhysicalFragment*> fragment_resetter( + ¤t_fragment_, child.get()); + Visit(); + + if (should_stop_traversing_) + return; } } -} -// The filter for CollectInlineFragments() collecting all fragments traversed. -class AddAllFilter { + private: + const NGPhysicalFragment* root_fragment_ = nullptr; + const NGPhysicalFragment* current_fragment_ = nullptr; + NGPhysicalOffset current_offset_to_root_; + Vector<Result> results_; + bool should_stop_traversing_ = false; + + DISALLOW_COPY_AND_ASSIGN(NGPhysicalFragmentCollectorBase); +}; + +// The visitor emitting all visited fragments. +class DescendantCollector final : public NGPhysicalFragmentCollectorBase { + STACK_ALLOCATED(); + public: - bool AddOnEnter(const NGPhysicalFragment*) const { return true; } - bool RemoveOnExit(const NGPhysicalFragment*) const { return false; } + DescendantCollector() = default; + + Vector<Result> CollectFrom(const NGPhysicalFragment& fragment) final { + return CollectExclusivelyFrom(fragment); + } + + private: + void Visit() final { + Emit(); + VisitChildren(); + } + + DISALLOW_COPY_AND_ASSIGN(DescendantCollector); }; -// The filter for CollectInlineFragments() collecting fragments generated from -// the given LayoutInline with supporting culled inline. +// The visitor emitting all visited fragments. +class InclusiveDescendantCollector final + : public NGPhysicalFragmentCollectorBase { + STACK_ALLOCATED(); + + public: + InclusiveDescendantCollector() = default; + + Vector<Result> CollectFrom(const NGPhysicalFragment& fragment) final { + return CollectInclusivelyFrom(fragment); + } + + private: + void Visit() final { + Emit(); + VisitChildren(); + } + + DISALLOW_COPY_AND_ASSIGN(InclusiveDescendantCollector); +}; + +// The visitor emitting fragments generated from the given LayoutInline, +// supporting culled inline. // Note: Since we apply culled inline per line, we have a fragment for // LayoutInline in second line but not in first line in // "t0803-c5502-imrgn-r-01-b-ag.html". -class LayoutInlineFilter { +class LayoutInlineCollector final : public NGPhysicalFragmentCollectorBase { + STACK_ALLOCATED(); + public: - explicit LayoutInlineFilter(const LayoutInline& container) { - CollectInclusiveDescendnats(container); + explicit LayoutInlineCollector(const LayoutInline& container) { + CollectInclusiveDescendants(container); } - bool AddOnEnter(const NGPhysicalFragment* fragment) { - if (fragment->IsLineBox()) - return false; - return inclusive_descendants_.Contains(fragment->GetLayoutObject()); + Vector<Result> CollectFrom(const NGPhysicalFragment& fragment) final { + return CollectExclusivelyFrom(fragment); } - bool RemoveOnExit(const NGPhysicalFragment*) const { return false; } - private: - void CollectInclusiveDescendnats(const LayoutInline& container) { + void Visit() final { + if (!GetFragment().IsLineBox() && + inclusive_descendants_.Contains(GetFragment().GetLayoutObject())) { + Emit(); + return; + } + VisitChildren(); + } + + void CollectInclusiveDescendants(const LayoutInline& container) { inclusive_descendants_.insert(&container); for (const LayoutObject* node = container.FirstChild(); node; node = node->NextSibling()) { @@ -89,127 +172,136 @@ class LayoutInlineFilter { } if (!node->IsLayoutInline()) continue; - CollectInclusiveDescendnats(ToLayoutInline(*node)); + CollectInclusiveDescendants(ToLayoutInline(*node)); } } HashSet<const LayoutObject*> inclusive_descendants_; + + DISALLOW_COPY_AND_ASSIGN(LayoutInlineCollector); }; -// The filter for CollectInlineFragments() collecting fragments generated from -// the given LayoutObject. -class LayoutObjectFilter { +// The visitor emitting all fragments generated from the given LayoutObject. +class LayoutObjectCollector final : public NGPhysicalFragmentCollectorBase { + STACK_ALLOCATED(); + public: - explicit LayoutObjectFilter(const LayoutObject* layout_object) - : layout_object_(layout_object) { - DCHECK(layout_object); - } + explicit LayoutObjectCollector(const LayoutObject* layout_object) + : target_(layout_object) {} - bool AddOnEnter(const NGPhysicalFragment* fragment) const { - return fragment->GetLayoutObject() == layout_object_; + Vector<Result> CollectFrom(const NGPhysicalFragment& fragment) final { + return CollectExclusivelyFrom(fragment); } - bool RemoveOnExit(const NGPhysicalFragment*) const { return false; } private: - const LayoutObject* layout_object_; + void Visit() final { + if (GetFragment().GetLayoutObject() == target_) + Emit(); + VisitChildren(); + } + + const LayoutObject* target_; + + DISALLOW_COPY_AND_ASSIGN(LayoutObjectCollector); }; -// The filter for CollectInlineFragments() collecting inclusive ancestors of the -// given fragment with the algorithm that, |fragment| is an ancestor of |target| -// if and only if both of the following are true: -// - |fragment| precedes |target| in preorder traversal -// - |fragment| succeeds |target| in postorder traversal -class InclusiveAncestorFilter { +// The visitor emitting ancestors of the given fragment in bottom-up order. +class AncestorCollector : public NGPhysicalFragmentCollectorBase { + STACK_ALLOCATED(); + public: - explicit InclusiveAncestorFilter(const NGPhysicalFragment& target) - : target_(&target) {} + explicit AncestorCollector(const NGPhysicalFragment& target) + : target_(target) {} - bool AddOnEnter(const NGPhysicalFragment* fragment) { - if (fragment == target_) - has_entered_target_ = true; - ancestors_precede_in_preorder_.push_back(!has_entered_target_); - return true; + Vector<Result> CollectFrom(const NGPhysicalFragment& fragment) final { + // TODO(xiaochengh): Change this into CollectInclusivlyFrom() to include + // subtree root to align with NodeTraversal::AncestorsOf(). + return CollectExclusivelyFrom(fragment); } - bool RemoveOnExit(const NGPhysicalFragment* fragment) { - if (fragment != target_) { - const bool precedes_in_preorder = ancestors_precede_in_preorder_.back(); - ancestors_precede_in_preorder_.pop_back(); - return !precedes_in_preorder || !has_exited_target_; + private: + void Visit() final { + if (&GetFragment() == &target_) { + SetShouldStopTraversing(); + return; } - has_exited_target_ = true; - ancestors_precede_in_preorder_.pop_back(); - return false; + + VisitChildren(); + if (HasStoppedTraversing()) + Emit(); + } + + const NGPhysicalFragment& target_; +}; + +// The visitor emitting inclusive ancestors of the given fragment in bottom-up +// order. +class InclusiveAncestorCollector : public NGPhysicalFragmentCollectorBase { + STACK_ALLOCATED(); + + public: + explicit InclusiveAncestorCollector(const NGPhysicalFragment& target) + : target_(target) {} + + Vector<Result> CollectFrom(const NGPhysicalFragment& fragment) final { + // TODO(xiaochengh): Change this into CollectInclusivlyFrom() to include + // subtree root to align with NodeTraversal::InclusiveAncestorsOf(). + return CollectExclusivelyFrom(fragment); } private: - const NGPhysicalFragment* target_; + void Visit() final { + if (&GetFragment() == &target_) { + SetShouldStopTraversing(); + Emit(); + return; + } - bool has_entered_target_ = false; - bool has_exited_target_ = false; + VisitChildren(); + if (HasStoppedTraversing()) + Emit(); + } - // For each currently entered but not-yet-exited fragment, stores a boolean of - // whether it precedes |target_| in preorder. - Vector<bool> ancestors_precede_in_preorder_; + const NGPhysicalFragment& target_; }; } // namespace // static -Vector<Result, 1> NGInlineFragmentTraversal::SelfFragmentsOf( +Vector<Result> NGInlineFragmentTraversal::SelfFragmentsOf( const NGPhysicalContainerFragment& container, const LayoutObject* layout_object) { if (layout_object->IsLayoutInline()) { - LayoutInlineFilter filter(*ToLayoutInline(layout_object)); - Vector<Result, 1> results; - CollectInlineFragments(container, {}, filter, &results); - return results; - } - LayoutObjectFilter filter(layout_object); - Vector<Result, 1> results; - CollectInlineFragments(container, {}, filter, &results); - return results; + return LayoutInlineCollector(ToLayoutInline(*layout_object)) + .CollectFrom(container); + } + return LayoutObjectCollector(layout_object).CollectFrom(container); } // static Vector<Result> NGInlineFragmentTraversal::DescendantsOf( const NGPhysicalContainerFragment& container) { - AddAllFilter add_all; - Vector<Result> results; - CollectInlineFragments(container, {}, add_all, &results); - return results; + return DescendantCollector().CollectFrom(container); } // static Vector<Result> NGInlineFragmentTraversal::InclusiveDescendantsOf( const NGPhysicalFragment& root) { - Vector<Result> results = - root.IsContainer() ? DescendantsOf(ToNGPhysicalContainerFragment(root)) - : Vector<Result>(); - results.push_front(Result{&root, {}}); - return results; + return InclusiveDescendantCollector().CollectFrom(root); } // static Vector<Result> NGInlineFragmentTraversal::InclusiveAncestorsOf( const NGPhysicalContainerFragment& container, const NGPhysicalFragment& target) { - InclusiveAncestorFilter inclusive_ancestors_of(target); - Vector<Result> results; - CollectInlineFragments(container, {}, inclusive_ancestors_of, &results); - std::reverse(results.begin(), results.end()); - return results; + return InclusiveAncestorCollector(target).CollectFrom(container); } // static Vector<Result> NGInlineFragmentTraversal::AncestorsOf( const NGPhysicalContainerFragment& container, const NGPhysicalFragment& target) { - Vector<Result> results = InclusiveAncestorsOf(container, target); - DCHECK(results.size()); - DCHECK_EQ(results.front().fragment, &target); - results.erase(results.begin()); - return results; + return AncestorCollector(target).CollectFrom(container); } } // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal.h index 27fb2e44c87..ef8bfd61034 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal.h @@ -44,7 +44,7 @@ class CORE_EXPORT NGInlineFragmentTraversal { // Returns list of inline fragments produced from the specified LayoutObject. // The search is restricted in the subtree of |container|. - static Vector<NGPhysicalFragmentWithOffset, 1> SelfFragmentsOf( + static Vector<NGPhysicalFragmentWithOffset> SelfFragmentsOf( const NGPhysicalContainerFragment& container, const LayoutObject* target); }; diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal_test.cc index 40bf8c8431a..822aca55e9f 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal_test.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal_test.cc @@ -106,10 +106,7 @@ TEST_F(NGInlineFragmentTraversalTest, SelfFragmentsOf) { // <b> generates two box fragments since its content is in two lines. EXPECT_NEXT_BOX(iter, "filter"); - EXPECT_NEXT_TEXT(iter, "bar"); - EXPECT_NEXT_TEXT(iter, "\n"); EXPECT_NEXT_BOX(iter, "filter"); - EXPECT_NEXT_TEXT(iter, "baz"); EXPECT_EQ(iter, descendants.end()); } diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.cc index 8e5a6288ddf..4ae097be73a 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.cc @@ -37,6 +37,19 @@ bool IsInlineBoxEmpty(const ComputedStyle& style, return true; } +// TODO(xiaochengh): Deduplicate with a similar function in ng_paint_fragment.cc +// ::before, ::after and ::first-letter can be hit test targets. +bool CanBeHitTestTargetPseudoNodeStyle(const ComputedStyle& style) { + switch (style.StyleType()) { + case kPseudoIdBefore: + case kPseudoIdAfter: + case kPseudoIdFirstLetter: + return true; + default: + return false; + } +} + } // namespace NGInlineItem::NGInlineItem(NGInlineItemType type, @@ -47,17 +60,20 @@ NGInlineItem::NGInlineItem(NGInlineItemType type, bool end_may_collapse) : start_offset_(start), end_offset_(end), - script_(USCRIPT_INVALID_CODE), style_(style), layout_object_(layout_object), type_(type), + script_(0), + font_fallback_priority_(0), + render_orientation_(0), bidi_level_(UBIDI_LTR), shape_options_(kPreContext | kPostContext), is_empty_item_(false), should_create_box_fragment_(false), style_variant_(static_cast<unsigned>(NGStyleVariant::kStandard)), end_collapse_type_(kNotCollapsible), - end_may_collapse_(end_may_collapse) { + end_may_collapse_(end_may_collapse), + is_symbol_marker_(false) { DCHECK_GE(end, start); ComputeBoxProperties(); } @@ -68,18 +84,21 @@ NGInlineItem::NGInlineItem(const NGInlineItem& other, scoped_refptr<const ShapeResult> shape_result) : start_offset_(start), end_offset_(end), - script_(other.script_), shape_result_(shape_result), style_(other.style_), layout_object_(other.layout_object_), type_(other.type_), + script_(other.script_), + font_fallback_priority_(other.font_fallback_priority_), + render_orientation_(other.render_orientation_), bidi_level_(other.bidi_level_), shape_options_(other.shape_options_), is_empty_item_(other.is_empty_item_), should_create_box_fragment_(other.should_create_box_fragment_), style_variant_(other.style_variant_), end_collapse_type_(other.end_collapse_type_), - end_may_collapse_(other.end_may_collapse_) { + end_may_collapse_(other.end_may_collapse_), + is_symbol_marker_(other.is_symbol_marker_) { DCHECK_GE(end, start); } @@ -95,7 +114,7 @@ void NGInlineItem::ComputeBoxProperties() { if (type_ == NGInlineItem::kOpenTag) { DCHECK(style_ && layout_object_ && layout_object_->IsLayoutInline()); - if (layout_object_->HasBoxDecorationBackground() || style_->HasPadding() || + if (style_->HasBoxDecorationBackground() || style_->HasPadding() || style_->HasMargin()) { is_empty_item_ = IsInlineBoxEmpty(*style_, *layout_object_); should_create_box_fragment_ = true; @@ -104,7 +123,10 @@ void NGInlineItem::ComputeBoxProperties() { should_create_box_fragment_ = ToLayoutBoxModelObject(layout_object_)->HasSelfPaintingLayer() || style_->HasOutline() || style_->CanContainAbsolutePositionObjects() || - style_->CanContainFixedPositionObjects(false); + style_->CanContainFixedPositionObjects(false) || + ToLayoutBoxModelObject(layout_object_) + ->ShouldApplyPaintContainment() || + CanBeHitTestTargetPseudoNodeStyle(*style_); } return; } @@ -121,6 +143,96 @@ const char* NGInlineItem::NGInlineItemTypeToString(int val) const { return kNGInlineItemTypeStrings[val]; } +UScriptCode NGInlineItem::Script() const { + return script_ != kInvalidScript ? static_cast<UScriptCode>(script_) + : USCRIPT_INVALID_CODE; +} + +FontFallbackPriority NGInlineItem::GetFontFallbackPriority() const { + return static_cast<enum FontFallbackPriority>(font_fallback_priority_); +} + +OrientationIterator::RenderOrientation NGInlineItem::RenderOrientation() const { + return static_cast<OrientationIterator::RenderOrientation>( + render_orientation_); +} + +RunSegmenter::RunSegmenterRange NGInlineItem::CreateRunSegmenterRange() const { + return {start_offset_, end_offset_, Script(), RenderOrientation(), + GetFontFallbackPriority()}; +} + +bool NGInlineItem::EqualsRunSegment(const NGInlineItem& other) const { + return script_ == other.script_ && + font_fallback_priority_ == other.font_fallback_priority_ && + render_orientation_ == other.render_orientation_; +} + +void NGInlineItem::SetRunSegment(const RunSegmenter::RunSegmenterRange& range) { + DCHECK_EQ(Type(), NGInlineItem::kText); + + // Orientation should be set in a separate pass. See + // NGInlineNode::SegmentScriptRuns(). + DCHECK_EQ(range.render_orientation, OrientationIterator::kOrientationKeep); + + script_ = static_cast<unsigned>(range.script); + font_fallback_priority_ = static_cast<unsigned>(range.font_fallback_priority); + + // Ensure our bit fields are large enough by reading them back. + DCHECK_EQ(range.script, Script()); + DCHECK_EQ(range.font_fallback_priority, GetFontFallbackPriority()); +} + +void NGInlineItem::SetFontOrientation( + OrientationIterator::RenderOrientation orientation) { + DCHECK_EQ(Type(), NGInlineItem::kText); + + // Ensure the value can fit in the bit field. + DCHECK_LT(static_cast<unsigned>(orientation), 1u << 1); + + render_orientation_ = orientation != 0; +} + +unsigned NGInlineItem::PopulateItemsFromRun( + Vector<NGInlineItem>& items, + unsigned index, + const RunSegmenter::RunSegmenterRange& range) { + DCHECK_GE(range.end, items[index].start_offset_); + + for (;; index++) { + NGInlineItem& item = items[index]; + DCHECK_LE(item.start_offset_, range.end); + + if (item.Type() == NGInlineItem::kText) + item.SetRunSegment(range); + + if (range.end == item.end_offset_) + break; + if (range.end < item.end_offset_) { + Split(items, index, range.end); + break; + } + } + return index + 1; +} + +unsigned NGInlineItem::PopulateItemsFromFontOrientation( + Vector<NGInlineItem>& items, + unsigned index, + unsigned end_offset, + OrientationIterator::RenderOrientation orientation) { + // FontOrientaiton is set per item, end_offset should be within this item. + NGInlineItem& item = items[index]; + DCHECK_GE(end_offset, item.start_offset_); + DCHECK_LE(end_offset, item.end_offset_); + + item.SetFontOrientation(orientation); + + if (end_offset < item.end_offset_) + Split(items, index, end_offset); + return index + 1; +} + void NGInlineItem::SetBidiLevel(UBiDiLevel level) { // Invalidate ShapeResult because it depends on the resolved direction. if (DirectionFromLevel(level) != DirectionFromLevel(bidi_level_)) diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h index 1b40a3a53d7..2e2a046614a 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h @@ -9,6 +9,7 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h" #include "third_party/blink/renderer/core/layout/ng/ng_style_variant.h" #include "third_party/blink/renderer/core/style/computed_style.h" +#include "third_party/blink/renderer/platform/fonts/shaping/run_segmenter.h" #include "third_party/blink/renderer/platform/fonts/shaping/shape_result.h" #include "third_party/blink/renderer/platform/text/text_direction.h" @@ -77,7 +78,9 @@ class CORE_EXPORT NGInlineItem { NGInlineItemType Type() const { return static_cast<NGInlineItemType>(type_); } const char* NGInlineItemTypeToString(int val) const; - const ShapeResult* TextShapeResult() const { return shape_result_.get(); } + scoped_refptr<const ShapeResult> TextShapeResult() const { + return shape_result_; + } NGLayoutInlineShapeOptions ShapeOptions() const { return static_cast<NGLayoutInlineShapeOptions>(shape_options_); } @@ -100,7 +103,6 @@ class CORE_EXPORT NGInlineItem { // direction. UBiDiLevel BidiLevelForReorder() const; - UScriptCode GetScript() const { return script_; } const ComputedStyle* Style() const { return style_.get(); } LayoutObject* GetLayoutObject() const { return layout_object_; } @@ -129,6 +131,26 @@ class CORE_EXPORT NGInlineItem { bool EndMayCollapse() const { return end_may_collapse_; } static void Split(Vector<NGInlineItem>&, unsigned index, unsigned offset); + + // Get RunSegmenter properties. + UScriptCode Script() const; + FontFallbackPriority GetFontFallbackPriority() const; + OrientationIterator::RenderOrientation RenderOrientation() const; + RunSegmenter::RunSegmenterRange CreateRunSegmenterRange() const; + // Whether the other item has the same RunSegmenter properties or not. + bool EqualsRunSegment(const NGInlineItem&) const; + // Set RunSegmenter properties. + static unsigned PopulateItemsFromRun(Vector<NGInlineItem>&, + unsigned index, + const RunSegmenter::RunSegmenterRange&); + void SetRunSegment(const RunSegmenter::RunSegmenterRange&); + static unsigned PopulateItemsFromFontOrientation( + Vector<NGInlineItem>&, + unsigned index, + unsigned end_offset, + OrientationIterator::RenderOrientation); + void SetFontOrientation(OrientationIterator::RenderOrientation); + void SetBidiLevel(UBiDiLevel); static unsigned SetBidiLevel(Vector<NGInlineItem>&, unsigned index, @@ -138,6 +160,9 @@ class CORE_EXPORT NGInlineItem { void AssertOffset(unsigned offset) const; void AssertEndOffset(unsigned offset) const; + bool IsSymbolMarker() const { return is_symbol_marker_; } + void SetIsSymbolMarker(bool b) { is_symbol_marker_ = b; } + String ToString() const; private: @@ -145,19 +170,27 @@ class CORE_EXPORT NGInlineItem { unsigned start_offset_; unsigned end_offset_; - UScriptCode script_; scoped_refptr<const ShapeResult> shape_result_; scoped_refptr<const ComputedStyle> style_; LayoutObject* layout_object_; + // UScriptCode is -1 (USCRIPT_INVALID_CODE) to 177 as of ICU 60. + // This can be packed to 8 bits, by handling -1 separately. + static constexpr unsigned kScriptBits = 8; + static constexpr unsigned kInvalidScript = (1 << kScriptBits) - 1; + unsigned type_ : 4; - unsigned bidi_level_ : 8; // UBiDiLevel is defined as uint8_t. + unsigned script_ : kScriptBits; + unsigned font_fallback_priority_ : 2; // FontFallbackPriority. + unsigned render_orientation_ : 1; // RenderOrientation (excl. kInvalid.) + unsigned bidi_level_ : 8; // UBiDiLevel is defined as uint8_t. unsigned shape_options_ : 2; unsigned is_empty_item_ : 1; unsigned should_create_box_fragment_ : 1; unsigned style_variant_ : 2; unsigned end_collapse_type_ : 2; // NGCollapseType unsigned end_may_collapse_ : 1; + unsigned is_symbol_marker_ : 1; friend class NGInlineNode; }; diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item_result.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item_result.cc index 43cdd7b803a..50ee0dee064 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item_result.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item_result.cc @@ -15,8 +15,13 @@ NGInlineItemResult::NGInlineItemResult() NGInlineItemResult::NGInlineItemResult(const NGInlineItem* item, unsigned index, unsigned start, - unsigned end) - : item(item), item_index(index), start_offset(start), end_offset(end) {} + unsigned end, + bool should_create_line_box) + : item(item), + item_index(index), + start_offset(start), + end_offset(end), + should_create_line_box(should_create_line_box) {} void NGLineInfo::SetLineStyle(const NGInlineNode& node, const NGInlineItemsData& items_data, @@ -26,7 +31,7 @@ void NGLineInfo::SetLineStyle(const NGInlineNode& node, bool is_after_forced_break) { use_first_line_style_ = use_first_line_style; items_data_ = &items_data; - line_style_ = node.GetLayoutObject()->Style(use_first_line_style_); + line_style_ = node.GetLayoutBox()->Style(use_first_line_style_); if (line_style_->ShouldUseTextIndent(is_first_line, is_after_forced_break)) { // 'text-indent' applies to block container, and percentage is of its @@ -47,11 +52,13 @@ void NGLineInfo::SetLineStyle(const NGInlineNode& node, } #if DCHECK_IS_ON() -void NGInlineItemResult::CheckConsistency() const { +void NGInlineItemResult::CheckConsistency(bool during_line_break) const { DCHECK(item); if (item->Type() == NGInlineItem::kText) { - DCHECK(shape_result); DCHECK_LT(start_offset, end_offset); + if (during_line_break && !shape_result) + return; + DCHECK(shape_result); DCHECK_EQ(end_offset - start_offset, shape_result->NumCharacters()); DCHECK_EQ(start_offset, shape_result->StartIndexForResult()); DCHECK_EQ(end_offset, shape_result->EndIndexForResult()); @@ -59,6 +66,20 @@ void NGInlineItemResult::CheckConsistency() const { } #endif +LayoutUnit NGLineInfo::ComputeWidth() const { + LayoutUnit inline_size = TextIndent(); + for (const NGInlineItemResult& item_result : Results()) + inline_size += item_result.inline_size; + + if (UNLIKELY(line_end_fragment_)) { + inline_size += line_end_fragment_->Size() + .ConvertToLogical(LineStyle().GetWritingMode()) + .inline_size; + } + + return inline_size; +} + void NGLineInfo::SetLineBfcOffset(NGBfcOffset line_bfc_offset, LayoutUnit available_width, LayoutUnit width) { diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item_result.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item_result.h index 514ae11066d..70ecc25ca16 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item_result.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item_result.h @@ -51,12 +51,12 @@ struct CORE_EXPORT NGInlineItemResult { scoped_refptr<NGLayoutResult> layout_result; // Margins and padding for atomic inline items and open/close tags. - NGBoxStrut margins; - NGBoxStrut padding; + NGLineBoxStrut margins; + NGLineBoxStrut padding; // Borders/padding for open tags. - LayoutUnit borders_paddings_block_start; - LayoutUnit borders_paddings_block_end; + LayoutUnit borders_paddings_line_over; + LayoutUnit borders_paddings_line_under; // Has start/end edge for open/close tags. bool has_edge = false; @@ -78,6 +78,22 @@ struct CORE_EXPORT NGInlineItemResult { // characters. bool has_only_trailing_spaces = false; + // We don't create "certain zero-height line boxes". + // https://drafts.csswg.org/css2/visuren.html#phantom-line-box + // Such line boxes do not prevent two margins being "adjoining", and thus + // collapsing. + // https://drafts.csswg.org/css2/box.html#collapsing-margins + // + // This field should be initialized to the previous value in the + // NGInlineItemResults list. If line breaker rewinds NGInlineItemResults + // list, we can still look at the last value in the list to determine if we + // need a line box. E.g. + // [float should_create_line_box: false], [text should_create_line_box: true] + // + // If "text" doesn't fit, and we rewind so that we only have "float", we can + // correctly determine that we don't need a line box. + bool should_create_line_box = false; + // End effects for text items. // The effects are included in |shape_result|, but not in text content. NGTextEndEffect text_end_effect = NGTextEndEffect::kNone; @@ -86,10 +102,11 @@ struct CORE_EXPORT NGInlineItemResult { NGInlineItemResult(const NGInlineItem*, unsigned index, unsigned start, - unsigned end); + unsigned end, + bool should_create_line_box); #if DCHECK_IS_ON() - void CheckConsistency() const; + void CheckConsistency(bool during_line_break = false) const; #endif }; @@ -105,9 +122,6 @@ class CORE_EXPORT NGLineInfo { DISALLOW_NEW_EXCEPT_PLACEMENT_NEW(); public: - NGLineInfo() = default; - explicit NGLineInfo(size_t capacity) : results_(capacity) {} - const NGInlineItemsData& ItemsData() const { DCHECK(items_data_); return *items_data_; @@ -137,8 +151,14 @@ class CORE_EXPORT NGLineInfo { bool IsLastLine() const { return is_last_line_; } void SetIsLastLine(bool is_last_line) { is_last_line_ = is_last_line; } + // If the line is marked as empty, it means that there's no content that + // requires it to be present at all, e.g. when there are only close tags with + // no margin/border/padding. + bool IsEmptyLine() const { return is_empty_line_; } + void SetIsEmptyLine() { is_empty_line_ = true; } + // NGInlineItemResults for this line. - NGInlineItemResults& Results() { return results_; } + NGInlineItemResults* MutableResults() { return &results_; } const NGInlineItemResults& Results() const { return results_; } LayoutUnit TextIndent() const { return text_indent_; } @@ -146,6 +166,7 @@ class CORE_EXPORT NGLineInfo { NGBfcOffset LineBfcOffset() const { return line_bfc_offset_; } LayoutUnit AvailableWidth() const { return available_width_; } LayoutUnit Width() const { return width_; } + LayoutUnit ComputeWidth() const; void SetLineBfcOffset(NGBfcOffset line_bfc_offset, LayoutUnit available_width, LayoutUnit width); @@ -183,6 +204,7 @@ class CORE_EXPORT NGLineInfo { bool use_first_line_style_ = false; bool is_last_line_ = false; + bool is_empty_line_ = false; }; } // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.cc index 7fad403e72e..ae1cb609e0a 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.cc @@ -95,19 +95,21 @@ bool ShouldRemoveNewlineSlow(const StringBuilder& before, if (before.Is8Bit() || after.Is8Bit()) return false; - // Remove if East Asian Widths of both before/after the newline are Wide. + // Remove if East Asian Widths of both before/after the newline are Wide, and + // neither side is Hangul. + // TODO(layout-dev): Don't remove if any side is Emoji. if (U16_IS_TRAIL(last) && space_index >= 2) { UChar last_last = before[space_index - 2]; if (U16_IS_LEAD(last_last)) last = U16_GET_SUPPLEMENTARY(last_last, last); } - if (IsEastAsianWidthWide(last, before_style)) { + if (!Character::IsHangul(last) && IsEastAsianWidthWide(last, before_style)) { if (U16_IS_LEAD(next) && after.length() > 1) { UChar next_next = after[1]; if (U16_IS_TRAIL(next_next)) next = U16_GET_SUPPLEMENTARY(next, next_next); } - if (IsEastAsianWidthWide(next, after_style)) + if (!Character::IsHangul(next) && IsEastAsianWidthWide(next, after_style)) return true; } @@ -799,6 +801,13 @@ void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::Exit( } } +template <typename OffsetMappingBuilder> +void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::SetIsSymbolMarker( + bool b) { + DCHECK(!items_->IsEmpty()); + items_->back().SetIsSymbolMarker(b); +} + template class CORE_TEMPLATE_EXPORT NGInlineItemsBuilderTemplate<EmptyOffsetMappingBuilder>; template class CORE_TEMPLATE_EXPORT diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.h index 53e34b6cef3..e944f9ac38f 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.h @@ -36,7 +36,7 @@ class LayoutText; // offsets in |text_|. // See https://goo.gl/CJbxky for more details about offset mapping. template <typename OffsetMappingBuilder> -class CORE_TEMPLATE_CLASS_EXPORT NGInlineItemsBuilderTemplate { +class NGInlineItemsBuilderTemplate { STACK_ALLOCATED(); public: @@ -107,6 +107,8 @@ class CORE_TEMPLATE_CLASS_EXPORT NGInlineItemsBuilderTemplate { OffsetMappingBuilder& GetOffsetMappingBuilder() { return mapping_builder_; } + void SetIsSymbolMarker(bool b); + private: Vector<NGInlineItem>* items_; StringBuilder text_; @@ -155,10 +157,11 @@ class CORE_TEMPLATE_CLASS_EXPORT NGInlineItemsBuilderTemplate { }; template <> -String NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::ToString(); +CORE_EXPORT String +NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::ToString(); template <> -bool NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::Append( +CORE_EXPORT bool NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::Append( const String&, LayoutObject*, const Vector<NGInlineItem*>&); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder_test.cc index ae5a273962c..1d75a511be4 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder_test.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder_test.cc @@ -68,10 +68,13 @@ class NGInlineItemsBuilderTest : public PageTestBase { const String& TestAppend(Vector<Input> inputs) { items_.clear(); + Vector<LayoutText*> anonymous_objects; NGInlineItemsBuilderForOffsetMapping builder(&items_); for (Input& input : inputs) { - if (!input.layout_text) + if (!input.layout_text) { input.layout_text = LayoutText::CreateEmptyAnonymous(GetDocument()); + anonymous_objects.push_back(input.layout_text); + } builder.Append(input.text, GetStyle(input.whitespace).get(), input.layout_text); } @@ -79,6 +82,8 @@ class NGInlineItemsBuilderTest : public PageTestBase { collapsed_ = GetCollapsed(builder.GetOffsetMappingBuilder()); ValidateItems(); CheckReuseItemsProducesSameResult(inputs); + for (LayoutObject* anonymous_object : anonymous_objects) + anonymous_object->Destroy(); return text_; } diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.cc index 0d04530a405..303800dd4db 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.cc @@ -50,20 +50,31 @@ struct NGLineAlign { NGLineAlign::NGLineAlign(const NGLineInfo& line_info) { space = line_info.AvailableWidth() - line_info.Width(); - // Eliminate trailing spaces from the alignment space. + // Compute the end text offset of this line for the alignment purpose. + // Trailing spaces are not part of the alignment space even when they are + // preserved. const NGInlineItemResults& item_results = line_info.Results(); for (auto it = item_results.rbegin(); it != item_results.rend(); ++it) { const NGInlineItemResult& item_result = *it; - if (!item_result.has_only_trailing_spaces) { - end_offset = item_result.end_offset; - space += trailing_spaces_width; - return; + + // If this item is opaque to whitespace collapsing, whitespace before this + // item maybe collapsed. Keep looking for previous items. + if (item_result.item && item_result.item->EndCollapseType() == + NGInlineItem::kOpaqueToCollapsing) { + continue; + } + + if (item_result.has_only_trailing_spaces) { + trailing_spaces_width += item_result.inline_size; + continue; } - trailing_spaces_width += item_result.inline_size; + + end_offset = item_result.end_offset; + space += trailing_spaces_width; + return; } // An empty line, or only trailing spaces. - DCHECK_EQ(space, line_info.AvailableWidth() - line_info.TextIndent()); end_offset = line_info.StartOffset(); space += trailing_spaces_width; } @@ -153,7 +164,8 @@ void NGInlineLayoutAlgorithm::PrepareBoxStates( void NGInlineLayoutAlgorithm::CreateLine(NGLineInfo* line_info, NGExclusionSpace* exclusion_space) { - NGInlineItemResults* line_items = &line_info->Results(); + // Needs MutableResults to move ShapeResult out of the NGLineInfo. + NGInlineItemResults* line_items = line_info->MutableResults(); line_box_.clear(); // Apply justification before placing items, because it affects size/position @@ -177,7 +189,13 @@ void NGInlineLayoutAlgorithm::CreateLine(NGLineInfo* line_info, NGInlineBoxState* box = box_states_->OnBeginPlaceItems(&line_style, baseline_type_, quirks_mode_); - for (auto& item_result : *line_items) { + // In order to match other browsers when list-style-type: none, pretend + // there's an invisible marker here. + if (line_style.Display() == EDisplay::kListItem && + line_style.ListStyleType() == EListStyleType::kNone) + box->ComputeTextMetrics(line_style, baseline_type_); + + for (NGInlineItemResult& item_result : *line_items) { DCHECK(item_result.item); const NGInlineItem& item = *item_result.item; if (item.Type() == NGInlineItem::kText) { @@ -195,9 +213,15 @@ void NGInlineLayoutAlgorithm::CreateLine(NGLineInfo* line_info, baseline_type_); } - text_builder.SetItem(NGPhysicalTextFragment::kNormalText, - line_info->ItemsData(), &item_result, - box->text_height); + if (item.IsSymbolMarker()) { + text_builder.SetItem(NGPhysicalTextFragment::kSymbolMarker, + line_info->ItemsData(), &item_result, + box->text_height); + } else { + text_builder.SetItem(NGPhysicalTextFragment::kNormalText, + line_info->ItemsData(), &item_result, + box->text_height); + } line_box_.AddChild(text_builder.ToTextFragment(), box->text_top, item_result.inline_size, item.BidiLevel()); } else if (item.Type() == NGInlineItem::kControl) { @@ -234,9 +258,10 @@ void NGInlineLayoutAlgorithm::CreateLine(NGLineInfo* line_info, IsLtr(line_info->BaseDirection()) ? 0 : 1, box); } - if (line_box_.IsEmpty() && !container_builder_.UnpositionedListMarker()) { - return; // The line was empty. - } + // We can return early if we don't have any children (and don't need to + // create a line-box for a list marker, etc). + if (line_box_.IsEmpty() && line_info->IsEmptyLine()) + return; box_states_->OnEndPlaceItems(&line_box_, baseline_type_); @@ -267,20 +292,17 @@ void NGInlineLayoutAlgorithm::CreateLine(NGLineInfo* line_info, // Handle out-of-flow positioned objects. They need inline offsets for their // static positions. - if (!PlaceOutOfFlowObjects(*line_info, line_box_metrics) && - !container_builder_.UnpositionedListMarker()) { - // If we have out-of-flow objects but nothing else, we don't have line box - // metrics nor BFC offset. Exit early. + PlaceOutOfFlowObjects(*line_info, line_box_metrics); + + // Even if we have something in-flow, it may just be empty items that + // shouldn't trigger creation of a line. Exit now if that's the case. + if (line_info->IsEmptyLine()) return; - } DCHECK(!line_box_metrics.IsEmpty()); NGBfcOffset line_bfc_offset(line_info->LineBfcOffset()); - // TODO(kojii): Implement flipped line (vertical-lr). In this case, line_top - // and block_start do not match. - // Up until this point, children are placed so that the dominant baseline is // at 0. Move them to the final baseline position, and set the logical top of // the line box to the line top. @@ -411,7 +433,7 @@ void NGInlineLayoutAlgorithm::PlaceLayoutResult(NGInlineItemResult* item_result, if (box) box->metrics.Unite(metrics); - LayoutUnit line_top = item_result->margins.block_start - metrics.ascent; + LayoutUnit line_top = item_result->margins.line_over - metrics.ascent; line_box_.AddChild(std::move(item_result->layout_result), NGLogicalOffset{inline_offset, line_top}, item_result->inline_size, item.BidiLevel()); @@ -419,21 +441,29 @@ void NGInlineLayoutAlgorithm::PlaceLayoutResult(NGInlineItemResult* item_result, // Place all out-of-flow objects in |line_box_| and clear them. // @return whether |line_box_| has any in-flow fragments. -bool NGInlineLayoutAlgorithm::PlaceOutOfFlowObjects( +void NGInlineLayoutAlgorithm::PlaceOutOfFlowObjects( const NGLineInfo& line_info, const NGLineHeightMetrics& line_box_metrics) { - bool has_fragments = false; - for (auto& child : line_box_) { + for (NGLineBoxFragmentBuilder::Child& child : line_box_) { if (LayoutObject* box = child.out_of_flow_positioned_box) { // The static position is at the line-top. Ignore the block_offset. NGLogicalOffset static_offset(child.offset.inline_offset, LayoutUnit()); // If a block-level box appears in the middle of a line, move the static // position to where the next block will be placed. - if (static_offset.inline_offset && - !box->StyleRef().IsOriginalDisplayInlineType()) { - static_offset.inline_offset = LayoutUnit(); - if (!line_box_metrics.IsEmpty()) + if (!box->StyleRef().IsOriginalDisplayInlineType()) { + LayoutUnit line_offset; + if (!line_info.IsEmptyLine()) { + line_offset = line_info.LineBfcOffset().line_offset - + ConstraintSpace().BfcOffset().line_offset; + } + line_offset += line_info.TextIndent(); + + // We need to subtract the line offset, in order to ignore + // floats and text-indent. + static_offset.inline_offset = -line_offset; + + if (child.offset.inline_offset && !line_box_metrics.IsEmpty()) static_offset.block_offset = line_box_metrics.LineHeight(); } @@ -443,11 +473,8 @@ bool NGInlineLayoutAlgorithm::PlaceOutOfFlowObjects( child.out_of_flow_positioned_box = child.out_of_flow_containing_box = nullptr; - } else if (!has_fragments) { - has_fragments = child.HasFragment(); } } - return has_fragments; } // Place a list marker. @@ -490,7 +517,7 @@ bool NGInlineLayoutAlgorithm::ApplyJustify(NGLineInfo* line_info) { if (!spacing.HasExpansion()) return false; // no expansion opportunities exist. - for (NGInlineItemResult& item_result : line_info->Results()) { + for (NGInlineItemResult& item_result : *line_info->MutableResults()) { if (item_result.has_only_trailing_spaces) break; if (item_result.shape_result) { @@ -508,8 +535,16 @@ bool NGInlineLayoutAlgorithm::ApplyJustify(NGLineInfo* line_info) { shape_result->StartIndexForResult()); item_result.inline_size = shape_result->SnappedWidth(); item_result.shape_result = std::move(shape_result); - } else { - // TODO(kojii): Implement atomic inline. + } else if (item_result.item->Type() == NGInlineItem::kAtomicInline) { + float offset = 0.f; + DCHECK_LE(line_info->StartOffset(), item_result.start_offset); + unsigned line_text_offset = + item_result.start_offset - line_info->StartOffset(); + DCHECK_EQ(kObjectReplacementCharacter, line_text[line_text_offset]); + float space = spacing.ComputeSpacing(line_text_offset, offset); + item_result.inline_size += space; + // |offset| is non-zero only before CJK characters. + DCHECK_EQ(offset, 0.f); } } return true; @@ -564,12 +599,21 @@ scoped_refptr<NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { bool is_empty_inline = Node().IsEmptyInline(); if (is_empty_inline) { + // Margins should collapse across "certain zero-height line boxes". + // https://drafts.csswg.org/css2/box.html#collapsing-margins + container_builder_.SetEndMarginStrut(ConstraintSpace().MarginStrut()); + // We're just going to collapse through this one, so whatever went in on one // side will go out on the other side. The position of the adjoining floats // will be affected by any subsequent block, until the BFC offset is // resolved. container_builder_.AddAdjoiningFloatTypes( ConstraintSpace().AdjoiningFloatTypes()); + + // For the empty lines, most of the logic here are not necessary, but in + // some edge cases we still need to create box fragments, such as when it + // has a containing block for out of flow objects. For now, use the code + // path than to create a fast code path for the stability. } else { DCHECK(ConstraintSpace().MarginStrut().IsEmpty()); container_builder_.SetBfcOffset(ConstraintSpace().BfcOffset()); @@ -584,26 +628,6 @@ scoped_refptr<NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { unsigned handled_item_index = PositionLeadingItems(initial_exclusion_space.get()); - // If we are an empty inline, we don't have to run the full algorithm, we can - // return now as we should have positioned all of our floats. - if (is_empty_inline) { - DCHECK_EQ(handled_item_index, Node().ItemsData(false).items.size()); - - container_builder_.SwapPositionedFloats(&positioned_floats_); - container_builder_.SetEndMarginStrut(ConstraintSpace().MarginStrut()); - container_builder_.SetExclusionSpace(std::move(initial_exclusion_space)); - - Vector<NGOutOfFlowPositionedDescendant> descendant_candidates; - container_builder_.GetAndClearOutOfFlowDescendantCandidates( - &descendant_candidates, nullptr); - for (auto& descendant : descendant_candidates) - container_builder_.AddOutOfFlowDescendant(descendant); - - return container_builder_.ToLineBoxFragment(); - } - - DCHECK(container_builder_.BfcOffset()); - // We query all the layout opportunities on the initial exclusion space up // front, as if the line breaker may add floats and change the opportunities. const Vector<NGLayoutOpportunity> opportunities = @@ -612,7 +636,8 @@ scoped_refptr<NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { ConstraintSpace().AvailableSize().inline_size); Vector<NGPositionedFloat> positioned_floats; - DCHECK(unpositioned_floats_.IsEmpty()); + // We shouldn't have any unpositioned floats if we aren't empty. + DCHECK(unpositioned_floats_.IsEmpty() || is_empty_inline); std::unique_ptr<NGExclusionSpace> exclusion_space; NGInlineBreakToken* break_token = BreakToken(); @@ -653,15 +678,12 @@ scoped_refptr<NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { line_block_size, block_delta); NGLineInfo line_info; - NGLineBreaker line_breaker( - Node(), NGLineBreakerMode::kContent, constraint_space_, - &positioned_floats, &unpositioned_floats_, &container_builder_, - exclusion_space.get(), handled_item_index, break_token); - - // TODO(ikilpatrick): Does this always succeed when we aren't an empty - // inline? - if (!line_breaker.NextLine(line_opportunity, &line_info)) - break; + NGLineBreaker line_breaker(Node(), NGLineBreakerMode::kContent, + constraint_space_, &positioned_floats, + &unpositioned_floats_, &container_builder_, + exclusion_space.get(), handled_item_index, + line_opportunity, break_token); + line_breaker.NextLine(&line_info); // If this fragment will be larger than the inline-size of the opportunity, // *and* the opportunity is smaller than the available inline-size, and the @@ -692,29 +714,35 @@ scoped_refptr<NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { // We now can check the block-size of the fragment, and it fits within the // opportunity. - LayoutUnit block_size = container_builder_.ComputeBlockSize(); + LayoutUnit line_height = container_builder_.LineHeight(); // Now that we have the block-size of the line, we can re-test the layout // opportunity to see if we fit into the (potentially) non-rectangular // shape area. + // // If the AvailableInlineSize changes we need to run the line breaker again // with the calculated line_block_size. This is *safe* as the line breaker // won't produce a line which has a larger block-size, (as it can only // decrease or stay the same size). - if (UNLIKELY(opportunity.HasShapeExclusions())) { + // + // We skip attempting to fit empty lines into the shape area, as they + // should only contain floats and/or abs-pos which shouldn't be affected by + // this logic. + if (UNLIKELY(opportunity.HasShapeExclusions() && + !line_info.IsEmptyLine())) { NGLineLayoutOpportunity line_opportunity_with_height = opportunity.ComputeLineLayoutOpportunity(ConstraintSpace(), - block_size, block_delta); + line_height, block_delta); if (line_opportunity_with_height.AvailableInlineSize() != line_opportunity.AvailableInlineSize()) { - line_block_size = block_size; + line_block_size = line_height; continue; } } // Check if the line will fit in the current opportunity. - if (block_size + block_delta > opportunity.rect.BlockSize()) { + if (line_height + block_delta > opportunity.rect.BlockSize()) { block_delta = LayoutUnit(); line_block_size = LayoutUnit(); ++opportunities_it; @@ -725,74 +753,64 @@ scoped_refptr<NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { ConstraintSpace().BfcOffset().block_offset) container_builder_.SetIsPushedByFloats(); - LayoutUnit line_height = container_builder_.LineHeight(); - // Success! positioned_floats_.AppendVector(positioned_floats); container_builder_.SetBreakToken( line_breaker.CreateBreakToken(line_info, std::move(box_states_))); - // Place any remaining floats which couldn't fit on the line. - PositionPendingFloats(line_height, exclusion_space.get()); - - // A <br clear=both> will strech the line-box height, such that the - // block-end edge will clear any floats. - // TODO(ikilpatrick): Move this into ng_block_layout_algorithm. - container_builder_.SetBlockSize( - ComputeContentSize(line_info, *exclusion_space, line_height)); - + if (is_empty_inline) { + DCHECK_EQ(container_builder_.BlockSize(), 0); + } else { + // Place any remaining floats which couldn't fit on the line. + PositionPendingFloats(line_height, exclusion_space.get()); + + // A <br clear=both> will strech the line-box height, such that the + // block-end edge will clear any floats. + // TODO(ikilpatrick): Move this into ng_block_layout_algorithm. + container_builder_.SetBlockSize( + ComputeContentSize(line_info, *exclusion_space, line_height)); + } break; } // We shouldn't have any unpositioned floats if we aren't empty. - DCHECK(unpositioned_floats_.IsEmpty()); + DCHECK(unpositioned_floats_.IsEmpty() || is_empty_inline); container_builder_.SwapPositionedFloats(&positioned_floats_); container_builder_.SetExclusionSpace( exclusion_space ? std::move(exclusion_space) : std::move(initial_exclusion_space)); - - Vector<NGOutOfFlowPositionedDescendant> descendant_candidates; - container_builder_.GetAndClearOutOfFlowDescendantCandidates( - &descendant_candidates, nullptr); - for (auto& descendant : descendant_candidates) - container_builder_.AddOutOfFlowDescendant(descendant); + container_builder_.MoveOutOfFlowDescendantCandidatesToDescendants(); return container_builder_.ToLineBoxFragment(); } // This positions any "leading" floats within the given exclusion space. // If we are also an empty inline, it will add any out-of-flow descendants. -// TODO(ikilpatrick): Do we need to always add the OOFs here? unsigned NGInlineLayoutAlgorithm::PositionLeadingItems( NGExclusionSpace* exclusion_space) { const Vector<NGInlineItem>& items = Node().ItemsData(false).items; - bool is_empty_inline = Node().IsEmptyInline(); LayoutUnit bfc_line_offset = ConstraintSpace().BfcOffset().line_offset; unsigned index = BreakToken() ? BreakToken()->ItemIndex() : 0; for (; index < items.size(); ++index) { - const auto& item = items[index]; + const NGInlineItem& item = items[index]; if (item.Type() == NGInlineItem::kFloating) { NGBlockNode node(ToLayoutBox(item.GetLayoutObject())); NGBoxStrut margins = ComputeMarginsForContainer(ConstraintSpace(), node.Style()); - auto unpositioned_float = NGUnpositionedFloat::Create( - ConstraintSpace().AvailableSize(), - ConstraintSpace().PercentageResolutionSize(), bfc_line_offset, - bfc_line_offset, margins, node, /* break_token */ nullptr); + scoped_refptr<NGUnpositionedFloat> unpositioned_float = + NGUnpositionedFloat::Create( + ConstraintSpace().AvailableSize(), + ConstraintSpace().PercentageResolutionSize(), bfc_line_offset, + bfc_line_offset, margins, node, /* break_token */ nullptr); AddUnpositionedFloat(&unpositioned_floats_, &container_builder_, std::move(unpositioned_float)); - } else if (is_empty_inline && - item.Type() == NGInlineItem::kOutOfFlowPositioned) { - NGBlockNode node(ToLayoutBox(item.GetLayoutObject())); - container_builder_.AddInlineOutOfFlowChildCandidate( - node, NGLogicalOffset(), Style().Direction(), nullptr); } // Abort if we've found something that makes this a non-empty inline. if (!item.IsEmptyItem()) { - DCHECK(!is_empty_inline); + DCHECK(!Node().IsEmptyInline()); break; } } @@ -821,7 +839,7 @@ void NGInlineLayoutAlgorithm::PositionPendingFloats( LayoutUnit origin_block_offset = bfc_offset.block_offset + content_size; LayoutUnit from_block_offset = bfc_offset.block_offset; - const auto positioned_floats = + const Vector<NGPositionedFloat> positioned_floats = PositionFloats(origin_block_offset, from_block_offset, unpositioned_floats_, ConstraintSpace(), exclusion_space); @@ -843,8 +861,8 @@ void NGInlineLayoutAlgorithm::BidiReorder() { Vector<UBiDiLevel, 32> levels; logical_items.ReserveInitialCapacity(line_box_.size()); levels.ReserveInitialCapacity(line_box_.size()); - for (auto& item : line_box_) { - if (!item.HasFragment() && !item.HasBidiLevel()) + for (NGLineBoxFragmentBuilder::Child& item : line_box_) { + if (item.IsPlaceholder()) continue; levels.push_back(item.bidi_level); logical_items.AddChild(std::move(item)); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.h index 017544f453a..4fcf7b1026a 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.h @@ -72,7 +72,7 @@ class CORE_EXPORT NGInlineLayoutAlgorithm final void PlaceLayoutResult(NGInlineItemResult*, NGInlineBoxState*, LayoutUnit inline_offset = LayoutUnit()); - bool PlaceOutOfFlowObjects(const NGLineInfo&, const NGLineHeightMetrics&); + void PlaceOutOfFlowObjects(const NGLineInfo&, const NGLineHeightMetrics&); void PlaceListMarker(const NGInlineItem&, NGInlineItemResult*, const NGLineInfo&); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm_test.cc index 654b7eb887e..6476e38750a 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm_test.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm_test.cc @@ -266,10 +266,11 @@ TEST_F(NGInlineLayoutAlgorithmTest, TextFloatsAroundFloatsBefore) { ToNGPhysicalBoxFragment(html_fragment->Children()[0].get()); auto* container_fragment = ToNGPhysicalBoxFragment(body_fragment->Children()[0].get()); - auto* span_box_fragments_wrapper = - ToNGPhysicalBoxFragment(container_fragment->Children()[3].get()); Vector<NGPhysicalLineBoxFragment*> line_boxes; - for (const auto& child : span_box_fragments_wrapper->Children()) { + for (const auto& child : container_fragment->Children()) { + if (!child->IsLineBox()) + continue; + line_boxes.push_back(ToNGPhysicalLineBoxFragment(child.get())); } @@ -433,8 +434,8 @@ TEST_F(NGInlineLayoutAlgorithmTest, PositionFloatsWithMargins) { EXPECT_EQ(LayoutUnit(53), span->OffsetLeft()); } -// Test glyph bounding box causes visual overflow. -TEST_F(NGInlineLayoutAlgorithmTest, VisualRect) { +// Test glyph bounding box causes ink overflow. +TEST_F(NGInlineLayoutAlgorithmTest, InkOverflow) { LoadAhem(); SetBodyInnerHTML(R"HTML( <!DOCTYPE html> @@ -452,9 +453,9 @@ TEST_F(NGInlineLayoutAlgorithmTest, VisualRect) { EXPECT_EQ(LayoutUnit(10), box_fragment->Size().height); - NGPhysicalOffsetRect visual_rect = box_fragment->ContentsVisualRect(); - EXPECT_EQ(LayoutUnit(-5), visual_rect.offset.top); - EXPECT_EQ(LayoutUnit(20), visual_rect.size.height); + NGPhysicalOffsetRect ink_overflow = box_fragment->ContentsInkOverflow(); + EXPECT_EQ(LayoutUnit(-5), ink_overflow.offset.top); + EXPECT_EQ(LayoutUnit(20), ink_overflow.size.height); } TEST_F(NGInlineLayoutAlgorithmTest, diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.cc index bf394eba54f..9cecafec27e 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.cc @@ -8,6 +8,7 @@ #include <memory> #include "third_party/blink/renderer/core/layout/layout_block_flow.h" +#include "third_party/blink/renderer/core/layout/layout_inline.h" #include "third_party/blink/renderer/core/layout/layout_object.h" #include "third_party/blink/renderer/core/layout/layout_text.h" #include "third_party/blink/renderer/core/layout/ng/inline/layout_ng_text.h" @@ -19,13 +20,16 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h" #include "third_party/blink/renderer/core/layout/ng/legacy_layout_tree_walking.h" +#include "third_party/blink/renderer/core/layout/ng/list/layout_ng_list_item.h" +#include "third_party/blink/renderer/core/layout/ng/list/layout_ng_list_marker.h" #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h" #include "third_party/blink/renderer/core/layout/ng/ng_layout_result.h" #include "third_party/blink/renderer/core/layout/ng/ng_length_utils.h" #include "third_party/blink/renderer/core/layout/ng/ng_positioned_float.h" #include "third_party/blink/renderer/core/layout/ng/ng_unpositioned_float.h" #include "third_party/blink/renderer/core/style/computed_style.h" -#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h" +#include "third_party/blink/renderer/platform/fonts/shaping/harfbuzz_shaper.h" +#include "third_party/blink/renderer/platform/fonts/shaping/run_segmenter.h" #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h" #include "third_party/blink/renderer/platform/wtf/text/character_names.h" @@ -65,6 +69,9 @@ void CollectInlinesInternal( String* previous_text) { builder->EnterBlock(block->Style()); LayoutObject* node = GetLayoutObjectForFirstChildNode(block); + + const LayoutObject* symbol = + LayoutNGListItem::FindSymbolMarkerLayoutText(block); while (node) { if (node->IsText()) { LayoutText* layout_text = ToLayoutText(node); @@ -87,6 +94,10 @@ void CollectInlinesInternal( else builder->Append(layout_text->GetText(), node->Style(), layout_text); } + + if (symbol == layout_text) + builder->SetIsSymbolMarker(true); + ClearNeedsLayoutIfUpdatingLayout<OffsetMappingBuilder>(layout_text); } else if (node->IsFloating()) { @@ -195,19 +206,8 @@ bool NGInlineNode::InLineHeightQuirksMode() const { } bool NGInlineNode::CanContainFirstFormattedLine() const { - // TODO(kojii): In LayoutNG, leading OOF creates an anonymous block box, - // and that |LayoutBlockFlow::CanContainFirstFormattedLine()| does not work. - // crbug.com/734554 - LayoutObject* layout_object = GetLayoutBlockFlow(); - if (!layout_object->IsAnonymousBlock()) - return true; - for (;;) { - layout_object = layout_object->PreviousSibling(); - if (!layout_object) - return true; - if (!layout_object->IsFloatingOrOutOfFlowPositioned()) - return false; - } + DCHECK(GetLayoutBlockFlow()); + return GetLayoutBlockFlow()->CanContainFirstFormattedLine(); } NGInlineNodeData* NGInlineNode::MutableData() { @@ -326,6 +326,71 @@ void NGInlineNode::CollectInlines(NGInlineNodeData* data, } void NGInlineNode::SegmentText(NGInlineNodeData* data) { + SegmentBidiRuns(data); + SegmentScriptRuns(data); + SegmentFontOrientation(data); +} + +// Segment NGInlineItem by script, Emoji, and orientation using RunSegmenter. +void NGInlineNode::SegmentScriptRuns(NGInlineNodeData* data) { + if (data->text_content.Is8Bit() && !data->is_bidi_enabled_) { + if (data->items.size()) { + RunSegmenter::RunSegmenterRange range = { + 0u, data->text_content.length(), USCRIPT_LATIN, + OrientationIterator::kOrientationKeep, FontFallbackPriority::kText}; + NGInlineItem::PopulateItemsFromRun(data->items, 0, range); + } + return; + } + + // Segment by script and Emoji. + // Orientation is segmented separately, because it may vary by items. + Vector<NGInlineItem>& items = data->items; + String& text_content = data->text_content; + text_content.Ensure16Bit(); + RunSegmenter segmenter(text_content.Characters16(), text_content.length(), + FontOrientation::kHorizontal); + RunSegmenter::RunSegmenterRange range = RunSegmenter::NullRange(); + for (unsigned item_index = 0; segmenter.Consume(&range);) { + DCHECK_EQ(items[item_index].start_offset_, range.start); + item_index = NGInlineItem::PopulateItemsFromRun(items, item_index, range); + } +} + +void NGInlineNode::SegmentFontOrientation(NGInlineNodeData* data) { + // Segment by orientation, only if vertical writing mode and items with + // 'text-orientation: mixed'. + if (GetLayoutBlockFlow()->IsHorizontalWritingMode()) + return; + + Vector<NGInlineItem>& items = data->items; + String& text_content = data->text_content; + text_content.Ensure16Bit(); + + for (unsigned item_index = 0; item_index < items.size();) { + NGInlineItem& item = items[item_index]; + if (item.Type() != NGInlineItem::kText || + item.Style()->GetFont().GetFontDescription().Orientation() != + FontOrientation::kVerticalMixed) { + item_index++; + continue; + } + unsigned start_offset = item.StartOffset(); + OrientationIterator iterator(text_content.Characters16() + start_offset, + item.Length(), + FontOrientation::kVerticalMixed); + unsigned end_offset; + OrientationIterator::RenderOrientation orientation; + while (iterator.Consume(&end_offset, &orientation)) { + item_index = NGInlineItem::PopulateItemsFromFontOrientation( + items, item_index, end_offset + start_offset, orientation); + } + } +} + +// Segment bidi runs by resolving bidi embedding levels. +// http://unicode.org/reports/tr9/#Resolving_Embedding_Levels +void NGInlineNode::SegmentBidiRuns(NGInlineNodeData* data) { if (!data->is_bidi_enabled_) { data->SetBaseDirection(TextDirection::kLtr); return; @@ -370,8 +435,6 @@ void NGInlineNode::SegmentText(NGInlineNodeData* data) { void NGInlineNode::ShapeText(NGInlineItemsData* data, NGInlineItemsData* previous_data) { - // TODO(eae): Add support for shaping latin-1 text? - data->text_content.Ensure16Bit(); ShapeText(data->text_content, &data->items, previous_data ? &previous_data->text_content : nullptr); } @@ -380,7 +443,7 @@ void NGInlineNode::ShapeText(const String& text_content, Vector<NGInlineItem>* items, const String* previous_text) { // Provide full context of the entire node to the shaper. - HarfBuzzShaper shaper(text_content.Characters16(), text_content.length()); + HarfBuzzShaper shaper(text_content); ShapeResultSpacing<String> spacing(text_content); for (unsigned index = 0; index < items->size();) { @@ -405,9 +468,12 @@ void NGInlineNode::ShapeText(const String& text_content, if (item.Type() == NGInlineItem::kText) { // Shape adjacent items together if the font and direction matches to // allow ligatures and kerning to apply. + // Also run segment properties must match because NGInlineItem gives + // pre-segmented range to HarfBuzzShaper. // TODO(kojii): Figure out the exact conditions under which this // behavior is desirable. - if (font != item.Style()->GetFont() || direction != item.Direction()) + if (font != item.Style()->GetFont() || direction != item.Direction() || + !item.EqualsRunSegment(start_item)) break; end_offset = item.EndOffset(); } else if (item.Type() == NGInlineItem::kOpenTag || @@ -459,8 +525,11 @@ void NGInlineNode::ShapeText(const String& text_content, } // Shape each item with the full context of the entire node. - scoped_refptr<ShapeResult> shape_result = - shaper.Shape(&font, direction, start_item.StartOffset(), end_offset); + RunSegmenter::RunSegmenterRange range = + start_item.CreateRunSegmenterRange(); + range.end = end_offset; + scoped_refptr<ShapeResult> shape_result = shaper.Shape( + &font, direction, start_item.StartOffset(), end_offset, &range); if (UNLIKELY(spacing.SetSpacing(font.GetFontDescription()))) shape_result->ApplySpacing(spacing); @@ -494,7 +563,7 @@ void NGInlineNode::ShapeText(const String& text_content, void NGInlineNode::ShapeTextForFirstLineIfNeeded(NGInlineNodeData* data) { // First check if the document has any :first-line rules. DCHECK(!data->first_line_items_); - LayoutObject* layout_object = GetLayoutObject(); + LayoutObject* layout_object = GetLayoutBox(); if (!layout_object->GetDocument().GetStyleEngine().UsesFirstLineRules()) return; @@ -533,6 +602,22 @@ void NGInlineNode::ShapeTextForFirstLineIfNeeded(NGInlineNodeData* data) { } } + // Check if we have a first-line anonymous inline box. It is the first + // open-tag if we have. + for (auto& item : first_line_items->items) { + if (item.Type() == NGInlineItem::kOpenTag) { + if (item.layout_object_->IsAnonymous() && + item.layout_object_->IsLayoutInline() && + item.layout_object_->Parent() == GetLayoutBox() && + ToLayoutInline(item.layout_object_)->IsFirstLineAnonymous()) { + item.should_create_box_fragment_ = true; + } + break; + } + if (item.Type() != NGInlineItem::kBidiControl) + break; + } + // Re-shape if the font is different. if (needs_reshape || FirstLineNeedsReshape(*first_line_style, *block_style)) ShapeText(first_line_items.get()); @@ -583,26 +668,28 @@ static LayoutUnit ComputeContentSize(NGInlineNode node, Vector<scoped_refptr<NGUnpositionedFloat>> unpositioned_floats; scoped_refptr<NGInlineBreakToken> break_token; - NGLineInfo line_info; NGExclusionSpace empty_exclusion_space; NGLineLayoutOpportunity line_opportunity(available_inline_size); LayoutUnit result; LayoutUnit previous_floats_inline_size = input.float_left_inline_size + input.float_right_inline_size; + DCHECK_GE(previous_floats_inline_size, 0); while (!break_token || !break_token->IsFinished()) { unpositioned_floats.clear(); - NGLineBreaker line_breaker(node, mode, *space, &positioned_floats, - &unpositioned_floats, - nullptr /* container_builder */, - &empty_exclusion_space, 0u, break_token.get()); - if (!line_breaker.NextLine(line_opportunity, &line_info)) + NGLineInfo line_info; + NGLineBreaker line_breaker( + node, mode, *space, &positioned_floats, &unpositioned_floats, + nullptr /* container_builder */, &empty_exclusion_space, 0u, + line_opportunity, break_token.get()); + line_breaker.NextLine(&line_info); + + if (line_info.Results().IsEmpty()) break; break_token = line_breaker.CreateBreakToken(line_info, nullptr); - LayoutUnit inline_size = line_info.TextIndent(); - for (const NGInlineItemResult item_result : line_info.Results()) - inline_size += item_result.inline_size; + LayoutUnit inline_size = line_info.Width(); + DCHECK_EQ(inline_size, line_info.ComputeWidth().ClampNegativeToZero()); // There should be no positioned floats while determining the min/max sizes. DCHECK_EQ(positioned_floats.size(), 0u); @@ -643,7 +730,10 @@ static LayoutUnit ComputeContentSize(NGInlineNode node, floats_inline_size = LayoutUnit(); } - floats_inline_size += child_sizes.max_size + child_inline_margins; + // When negative margins move the float outside the content area, + // such float should not affect the content size. + floats_inline_size += + (child_sizes.max_size + child_inline_margins).ClampNegativeToZero(); previous_float_type = float_style.Floating(); } } diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h index 7685706584d..73d49d80621 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h @@ -80,6 +80,9 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode { void CollectInlines(NGInlineNodeData*, NGInlineNodeData* previous_data = nullptr); void SegmentText(NGInlineNodeData*); + void SegmentScriptRuns(NGInlineNodeData*); + void SegmentFontOrientation(NGInlineNodeData*); + void SegmentBidiRuns(NGInlineNodeData*); void ShapeText(NGInlineItemsData*, NGInlineItemsData* previous_data = nullptr); void ShapeText(const String& text, diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_test.cc index f04e5e70b56..c06d5dcefb0 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_test.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_test.cc @@ -359,7 +359,7 @@ TEST_F(NGInlineNodeTest, SegmentBidiIsolate) { NGInlineNodeForTest node = CreateInlineNode(); node = CreateBidiIsolateNode(node, style_.get(), layout_object_); Vector<NGInlineItem>& items = node.Items(); - ASSERT_EQ(9u, items.size()); + ASSERT_EQ(10u, items.size()); TEST_ITEM_OFFSET_DIR(items[0], 0u, 6u, TextDirection::kLtr); TEST_ITEM_OFFSET_DIR(items[1], 6u, 7u, TextDirection::kLtr); TEST_ITEM_OFFSET_DIR(items[2], 7u, 13u, TextDirection::kRtl); @@ -368,7 +368,8 @@ TEST_F(NGInlineNodeTest, SegmentBidiIsolate) { TEST_ITEM_OFFSET_DIR(items[5], 15u, 16u, TextDirection::kRtl); TEST_ITEM_OFFSET_DIR(items[6], 16u, 21u, TextDirection::kRtl); TEST_ITEM_OFFSET_DIR(items[7], 21u, 22u, TextDirection::kLtr); - TEST_ITEM_OFFSET_DIR(items[8], 22u, 28u, TextDirection::kLtr); + TEST_ITEM_OFFSET_DIR(items[8], 22u, 23u, TextDirection::kLtr); + TEST_ITEM_OFFSET_DIR(items[9], 23u, 28u, TextDirection::kLtr); } #define TEST_TEXT_FRAGMENT(fragment, start_offset, end_offset) \ @@ -385,12 +386,13 @@ TEST_F(NGInlineNodeTest, CreateLineBidiIsolate) { node.ShapeText(); Vector<scoped_refptr<const NGPhysicalTextFragment>> fragments; CreateLine(node, &fragments); - ASSERT_EQ(5u, fragments.size()); + ASSERT_EQ(6u, fragments.size()); TEST_TEXT_FRAGMENT(fragments[0], 0u, 6u); TEST_TEXT_FRAGMENT(fragments[1], 16u, 21u); TEST_TEXT_FRAGMENT(fragments[2], 14u, 15u); TEST_TEXT_FRAGMENT(fragments[3], 7u, 13u); - TEST_TEXT_FRAGMENT(fragments[4], 22u, 28u); + TEST_TEXT_FRAGMENT(fragments[4], 22u, 23u); + TEST_TEXT_FRAGMENT(fragments[5], 23u, 28u); } TEST_F(NGInlineNodeTest, MinMaxSize) { diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment_builder.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment_builder.cc index 2b7a7e3d58e..5e01951043f 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment_builder.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment_builder.cc @@ -36,19 +36,6 @@ LayoutUnit NGLineBoxFragmentBuilder::LineHeight() const { return metrics_.LineHeight().ClampNegativeToZero(); } -LayoutUnit NGLineBoxFragmentBuilder::ComputeBlockSize() const { - LayoutUnit block_size; - WritingMode writing_mode(node_.Style().GetWritingMode()); - - for (size_t i = 0; i < children_.size(); ++i) { - block_size = std::max( - block_size, offsets_[i].block_offset + - NGFragment(writing_mode, *children_[i]).BlockSize()); - } - - return block_size; -} - const NGPhysicalFragment* NGLineBoxFragmentBuilder::Child::PhysicalFragment() const { return layout_result ? layout_result->PhysicalFragment().get() @@ -142,25 +129,21 @@ void NGLineBoxFragmentBuilder::AddChildren(ChildList& children) { scoped_refptr<NGLayoutResult> NGLineBoxFragmentBuilder::ToLineBoxFragment() { DCHECK_EQ(offsets_.size(), children_.size()); - WritingMode writing_mode(node_.Style().GetWritingMode()); - NGPhysicalSize physical_size = Size().ConvertToPhysical(writing_mode); + WritingMode line_writing_mode(ToLineWritingMode(GetWritingMode())); + NGPhysicalSize physical_size = Size().ConvertToPhysical(line_writing_mode); - NGPhysicalOffsetRect contents_visual_rect({}, physical_size); - NGPhysicalOffsetRect scrollable_overflow({}, physical_size); + NGPhysicalOffsetRect contents_ink_overflow({}, physical_size); for (size_t i = 0; i < children_.size(); ++i) { NGPhysicalFragment* child = children_[i].get(); child->SetOffset(offsets_[i].ConvertToPhysical( - writing_mode, Direction(), physical_size, child->Size())); - child->PropagateContentsVisualRect(&contents_visual_rect); - NGPhysicalOffsetRect child_scroll_overflow = child->ScrollableOverflow(); - child_scroll_overflow.offset += child->Offset(); - scrollable_overflow.Unite(child_scroll_overflow); + line_writing_mode, Direction(), physical_size, child->Size())); + child->PropagateContentsInkOverflow(&contents_ink_overflow); } scoped_refptr<NGPhysicalLineBoxFragment> fragment = base::AdoptRef(new NGPhysicalLineBoxFragment( Style(), style_variant_, physical_size, children_, - contents_visual_rect, scrollable_overflow, metrics_, base_direction_, + contents_ink_overflow, metrics_, base_direction_, break_token_ ? std::move(break_token_) : NGInlineBreakToken::Create(node_))); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment_builder.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment_builder.h index 9ea29183fb1..be0369eaaf2 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment_builder.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment_builder.h @@ -22,6 +22,7 @@ struct NGPositionedFloat; class CORE_EXPORT NGLineBoxFragmentBuilder final : public NGContainerFragmentBuilder { STACK_ALLOCATED(); + public: NGLineBoxFragmentBuilder(NGInlineNode, scoped_refptr<const ComputedStyle>, @@ -32,7 +33,6 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final void Reset(); LayoutUnit LineHeight() const; - LayoutUnit ComputeBlockSize() const; const NGLineHeightMetrics& Metrics() const { return metrics_; } void SetMetrics(const NGLineHeightMetrics&); @@ -107,6 +107,7 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final return HasInFlowFragment() || HasOutOfFlowFragment(); } bool HasBidiLevel() const { return bidi_level != 0xff; } + bool IsPlaceholder() const { return !HasFragment() && !HasBidiLevel(); } const NGPhysicalFragment* PhysicalFragment() const; }; diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc index 79294415634..31fa4092c6d 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc @@ -4,6 +4,7 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.h" +#include "third_party/blink/renderer/core/layout/layout_list_marker.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" @@ -33,19 +34,12 @@ 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(); +inline bool ShouldCreateLineBox(const NGInlineItemResults& item_results) { + return !item_results.IsEmpty() && item_results.back().should_create_line_box; } +} // namespace + NGLineBreaker::NGLineBreaker( NGInlineNode node, NGLineBreakerMode mode, @@ -55,10 +49,20 @@ NGLineBreaker::NGLineBreaker( NGContainerFragmentBuilder* container_builder, NGExclusionSpace* exclusion_space, unsigned handled_float_index, + const NGLineLayoutOpportunity& line_opportunity, const NGInlineBreakToken* break_token) - : line_(node, break_token), + : line_opportunity_(line_opportunity), node_(node), - items_data_(node.ItemsData(line_.use_first_line_style)), + is_first_formatted_line_((!break_token || (!break_token->ItemIndex() && + !break_token->TextOffset())) && + node.CanContainFirstFormattedLine()), + use_first_line_style_(is_first_formatted_line_ && + node.GetLayoutBox() + ->GetDocument() + .GetStyleEngine() + .UsesFirstLineRules()), + in_line_height_quirks_mode_(node.InLineHeightQuirksMode()), + items_data_(node.ItemsData(use_first_line_style_)), mode_(mode), constraint_space_(space), positioned_floats_(positioned_floats), @@ -66,12 +70,10 @@ NGLineBreaker::NGLineBreaker( 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()), + shaper_(items_data_.text_content), spacing_(items_data_.text_content), handled_floats_end_item_index_(handled_float_index), - base_direction_(node_.BaseDirection()), - in_line_height_quirks_mode_(node.InLineHeightQuirksMode()) { + base_direction_(node_.BaseDirection()) { break_iterator_.SetBreakSpace(BreakSpaceType::kBeforeSpaceRun); if (break_token) { @@ -88,36 +90,32 @@ NGLineBreaker::NGLineBreaker( // header. NGLineBreaker::~NGLineBreaker() = default; -inline NGInlineItemResult* NGLineBreaker::AddItem( - const NGInlineItem& item, - unsigned end_offset, - NGInlineItemResults* item_results) { +inline NGInlineItemResult* NGLineBreaker::AddItem(const NGInlineItem& item, + unsigned end_offset) { DCHECK_LE(end_offset, item.EndOffset()); - item_results->push_back( - NGInlineItemResult(&item, item_index_, offset_, end_offset)); - return &item_results->back(); + item_results_->push_back( + NGInlineItemResult(&item, item_index_, offset_, end_offset, + ShouldCreateLineBox(*item_results_))); + return &item_results_->back(); } -inline NGInlineItemResult* NGLineBreaker::AddItem( - const NGInlineItem& item, - NGInlineItemResults* item_results) { - return AddItem(item, item.EndOffset(), item_results); +inline NGInlineItemResult* NGLineBreaker::AddItem(const NGInlineItem& item) { + return AddItem(item, item.EndOffset()); } void NGLineBreaker::SetLineEndFragment( - scoped_refptr<NGPhysicalTextFragment> fragment, - NGLineInfo* line_info) { + scoped_refptr<NGPhysicalTextFragment> fragment) { 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 (line_info_->LineEndFragment()) { + const NGPhysicalSize& size = line_info_->LineEndFragment()->Size(); + position_ -= is_horizontal ? size.width : size.height; } if (fragment) { const NGPhysicalSize& size = fragment->Size(); - line_.position += is_horizontal ? size.width : size.height; + position_ += is_horizontal ? size.width : size.height; } - line_info->SetLineEndFragment(std::move(fragment)); + line_info_->SetLineEndFragment(std::move(fragment)); } inline void NGLineBreaker::ComputeCanBreakAfter( @@ -126,27 +124,15 @@ inline void NGLineBreaker::ComputeCanBreakAfter( 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) { +void NGLineBreaker::ComputeBaseDirection() { // 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; + const String& text = Text(); if (text.Is8Bit()) return; size_t end_offset = text.find(kNewlineCharacter, offset_); @@ -157,67 +143,77 @@ void NGLineBreaker::ComputeBaseDirection(const NGLineInfo& line_info) { } // 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_); +void NGLineBreaker::PrepareNextLine() { + // NGLineInfo is not supposed to be re-used becase it's not much gain and to + // avoid rare code path. + DCHECK(item_results_->IsEmpty()); + + line_info_->SetStartOffset(offset_); + line_info_->SetLineStyle(node_, items_data_, constraint_space_, + is_first_formatted_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; + SetCurrentStyle(current_style_ ? *current_style_ : line_info_->LineStyle()); + ComputeBaseDirection(); + line_info_->SetBaseDirection(base_direction_); // 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; + position_ = line_info_->TextIndent(); } -bool NGLineBreaker::NextLine(const NGLineLayoutOpportunity& line_opportunity, - NGLineInfo* line_info) { - PrepareNextLine(line_opportunity, line_info); - BreakLine(line_info); +void NGLineBreaker::NextLine(NGLineInfo* line_info) { + line_info_ = line_info; + item_results_ = line_info->MutableResults(); + + PrepareNextLine(); + BreakLine(); + if (!trailing_spaces_collapsed_) + RemoveTrailingCollapsibleSpace(); - if (line_info->Results().IsEmpty()) - return false; +#if DCHECK_IS_ON() + for (const auto& result : *item_results_) + result.CheckConsistency(); +#endif + // We should create a line-box when: + // - We have an item which needs a line box (text, etc). + // - A list-marker is present, and it would be the last line or last line + // before a forced new-line. + // - During min/max content sizing (to correctly determine the line width). + // // 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; + if (ShouldCreateLineBox(*item_results_) || + (has_list_marker_ && line_info_->IsLastLine()) || + mode_ != NGLineBreakerMode::kContent) + ComputeLineLocation(); + else + line_info_->SetIsEmptyLine(); + + line_info_ = nullptr; + item_results_ = nullptr; } -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) { +void NGLineBreaker::BreakLine() { + const Vector<NGInlineItem>& items = Items(); + state_ = LineBreakState::kLeading; + 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 (state_ == LineBreakState::kContinue && auto_wrap_ && + position_ > AvailableWidthToFit()) { + HandleOverflow(); } // 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); + line_info_->SetIsLastLine(true); return; } @@ -225,55 +221,57 @@ void NGLineBreaker::BreakLine(NGLineInfo* line_info) { // 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); + HandleText(item); #if DCHECK_IS_ON() - if (!item_results->IsEmpty()) - item_results->back().CheckConsistency(); + if (!item_results_->IsEmpty()) + item_results_->back().CheckConsistency(true); #endif continue; } if (item.Type() == NGInlineItem::kCloseTag) { - HandleCloseTag(item, item_results); + HandleCloseTag(item); continue; } if (item.Type() == NGInlineItem::kControl) { - state = HandleControlItem(item, state, line_info); + HandleControlItem(item); + continue; + } + if (item.Type() == NGInlineItem::kFloating) { + HandleFloat(item); continue; } if (item.Type() == NGInlineItem::kBidiControl) { - state = HandleBidiControlItem(item, state, line_info); + HandleBidiControlItem(item); 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); + if (state_ == LineBreakState::kTrailing && + CanBreakAfterLast(*item_results_)) { + line_info_->SetIsLastLine(false); return; } if (item.Type() == NGInlineItem::kAtomicInline) { - HandleAtomicInline(item, line_info); + HandleAtomicInline(item); } 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)); + HandleOpenTag(item); } else if (item.Type() == NGInlineItem::kOutOfFlowPositioned) { DCHECK_EQ(item.Length(), 0u); - AddItem(item, item_results); + AddItem(item); 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); + NGInlineItemResult* item_result = AddItem(item); 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); + NGInlineItemResult* item_result = AddItem(item); + has_list_marker_ = true; DCHECK(!item_result->can_break_after); MoveToNextOf(item); } else { @@ -283,84 +281,111 @@ void NGLineBreaker::BreakLine(NGLineInfo* line_info) { } } -// Re-compute the current position from NGInlineItemResults. +// Re-compute the current position from NGLineInfo. // 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::UpdatePosition() { + position_ = line_info_->ComputeWidth(); } -void NGLineBreaker::ComputeLineLocation(NGLineInfo* line_info) const { - LayoutUnit bfc_line_offset = line_.line_opportunity.line_left_offset; - LayoutUnit available_width = line_.AvailableWidth(); +void NGLineBreaker::ComputeLineLocation() const { + LayoutUnit bfc_line_offset = line_opportunity_.line_left_offset; + LayoutUnit available_width = AvailableWidth(); + DCHECK_EQ(position_, line_info_->ComputeWidth()); // 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()); + line_info_->SetLineBfcOffset( + {bfc_line_offset, line_opportunity_.bfc_block_offset}, available_width, + position_.ClampNegativeToZero()); } -NGLineBreaker::LineBreakState NGLineBreaker::HandleText( - const NGInlineItem& item, - LineBreakState state, - NGLineInfo* line_info) { +void NGLineBreaker::HandleText(const NGInlineItem& item) { 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); + if (state_ == LineBreakState::kTrailing && + CanBreakAfterLast(*item_results_)) { + return HandleTrailingSpaces(item); } - line_.should_create_line_box = true; - NGInlineItemResult* item_result = AddItem(item, item_results); - LayoutUnit available_width = line_.AvailableWidth(); + // Skip leading collapsible spaces. + // Most cases such spaces are handled as trailing spaces of the previous line, + // but there are some cases doing so is too complex. + if (state_ == LineBreakState::kLeading) { + state_ = LineBreakState::kContinue; + if (item.Style()->CollapseWhiteSpace() && + Text()[offset_] == kSpaceCharacter) { + // Skipping one whitespace removes all collapsible spaces because + // collapsible spaces are collapsed to single space in + // NGInlineItemBuilder. + ++offset_; + if (offset_ == item.EndOffset()) { + MoveToNextOf(item); + return; + } + } + } + + NGInlineItemResult* item_result = AddItem(item); + item_result->should_create_line_box = true; + LayoutUnit available_width = AvailableWidthToFit(); 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; + BreakText(item_result, item, available_width - position_); + + if (item.IsSymbolMarker()) { + LayoutUnit symbol_width = LayoutListMarker::WidthOfSymbol(*item.Style()); + if (symbol_width > 0) + item_result->inline_size = symbol_width; + } + + LayoutUnit next_position = position_ + item_result->inline_size; bool is_overflow = next_position > available_width; - line_.position = next_position; + DCHECK(is_overflow || item_result->shape_result); + position_ = next_position; item_result->may_break_inside = !is_overflow; MoveToNextOf(*item_result); - if (!is_overflow || state == LineBreakState::kTrailing) { + if (!is_overflow || + (state_ == LineBreakState::kTrailing && item_result->shape_result)) { if (item_result->end_offset < item.EndOffset()) { // The break point found, and text follows. Break here, after trailing // spaces. - return HandleTrailingSpaces(item, line_info); + return HandleTrailingSpaces(item); } // The break point found, but items that prohibit breaking before them may // follow. Continue looking next items. - return state; + return; } - return HandleOverflow(line_info); + return HandleOverflow(); } // 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); + BreakText(item_result, item, LayoutUnit::Max()); + + if (item.IsSymbolMarker()) { + LayoutUnit symbol_width = LayoutListMarker::WidthOfSymbol(*item.Style()); + if (symbol_width > 0) + item_result->inline_size = symbol_width; + } + 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; + position_ += item_result->inline_size; MoveToNextOf(item); - return state; } void NGLineBreaker::BreakText(NGInlineItemResult* item_result, const NGInlineItem& item, - LayoutUnit available_width, - NGLineInfo* line_info) { + LayoutUnit available_width) { DCHECK_EQ(item.Type(), NGInlineItem::kText); item.AssertOffset(item_result->start_offset); @@ -370,19 +395,42 @@ void NGLineBreaker::BreakText(NGInlineItemResult* item_result, // expensive? DCHECK_EQ(item.TextShapeResult()->StartIndexForResult(), item.StartOffset()); DCHECK_EQ(item.TextShapeResult()->EndIndexForResult(), item.EndOffset()); + RunSegmenter::RunSegmenterRange segment_range = + item.CreateRunSegmenterRange(); ShapingLineBreaker breaker(&shaper_, &item.Style()->GetFont(), item.TextShapeResult(), &break_iterator_, - &spacing_, hyphenation_); + &segment_range, &spacing_, hyphenation_); if (!enable_soft_hyphen_) breaker.DisableSoftHyphen(); available_width = std::max(LayoutUnit(0), available_width); + + // Use kStartShouldBeSafe if at the beginning of a line. + unsigned options = ShapingLineBreaker::kDefaultOptions; + if (offset_ != line_info_->StartOffset()) + options |= ShapingLineBreaker::kDontReshapeStart; + + // Use kNoResultIfOverflow if 'break-word' and we're trying to break normally + // because if this item overflows, we will rewind and break line again. The + // overflowing ShapeResult is not needed. + if (break_anywhere_if_overflow_ && !override_break_anywhere_) + options |= ShapingLineBreaker::kNoResultIfOverflow; 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); + scoped_refptr<const ShapeResult> shape_result = breaker.ShapeLine( + item_result->start_offset, available_width, options, &result); + + // If this item overflows and 'break-word' is set, this line will be + // rewinded. Making this item long enough to overflow is enough. + if (!shape_result) { + DCHECK(options & ShapingLineBreaker::kNoResultIfOverflow); + item_result->inline_size = available_width + 1; + item_result->end_offset = item.EndOffset(); + return; + } + DCHECK_EQ(shape_result->NumCharacters(), + result.break_offset - item_result->start_offset); + if (result.is_hyphenated) { - AppendHyphen(item, line_info); + AppendHyphen(item); // 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; @@ -409,42 +457,103 @@ void NGLineBreaker::BreakText(NGInlineItemResult* item_result, } } -NGLineBreaker::LineBreakState NGLineBreaker::HandleTrailingSpaces( - const NGInlineItem& item, - NGLineInfo* line_info) { +// Re-shape the specified range of |NGInlineItem|. +scoped_refptr<ShapeResult> NGLineBreaker::ShapeText(const NGInlineItem& item, + unsigned start, + unsigned end) { + RunSegmenter::RunSegmenterRange segment_range = + item.CreateRunSegmenterRange(); + scoped_refptr<ShapeResult> result = shaper_.Shape( + &item.Style()->GetFont(), item.TextShapeResult()->Direction(), start, end, + &segment_range); + if (UNLIKELY(spacing_.HasSpacing())) + result->ApplySpacing(spacing_); + return result; +} + +// Compute a new ShapeResult for the specified end offset. +// The end is re-shaped if it is not safe-to-break. +scoped_refptr<ShapeResult> NGLineBreaker::TruncateLineEndResult( + const NGInlineItemResult& item_result, + unsigned end_offset) { + DCHECK(item_result.item); + DCHECK(item_result.shape_result); + const ShapeResult& source_result = *item_result.shape_result; + const unsigned start_offset = item_result.start_offset; + DCHECK_GE(start_offset, source_result.StartIndexForResult()); + DCHECK_LE(end_offset, source_result.EndIndexForResult()); + DCHECK(start_offset > source_result.StartIndexForResult() || + end_offset < source_result.EndIndexForResult()); + + scoped_refptr<ShapeResult> new_result; + unsigned last_safe = source_result.PreviousSafeToBreakOffset(end_offset); + DCHECK_LE(last_safe, end_offset); + if (last_safe > start_offset) + new_result = source_result.SubRange(start_offset, last_safe); + if (last_safe < end_offset) { + scoped_refptr<ShapeResult> end_result = ShapeText( + *item_result.item, std::max(last_safe, start_offset), end_offset); + if (new_result) + end_result->CopyRange(0, end_offset, new_result.get()); + else + new_result = std::move(end_result); + } + DCHECK(new_result); + return new_result; +} + +// Update |ShapeResult| in |item_result| to match to its |start_offset| and +// |end_offset|. The end is re-shaped if it is not safe-to-break. +void NGLineBreaker::UpdateShapeResult(NGInlineItemResult* item_result) { + DCHECK(item_result); + item_result->shape_result = + TruncateLineEndResult(*item_result, item_result->end_offset); + DCHECK(item_result->shape_result); + item_result->inline_size = item_result->shape_result->SnappedWidth(); +} + +void NGLineBreaker::HandleTrailingSpaces(const NGInlineItem& item) { 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; + if (text[offset_] != kSpaceCharacter) { + state_ = LineBreakState::kDone; + return; + } // Skipping one whitespace removes all collapsible spaces because // collapsible spaces are collapsed to single space in NGInlineItemBuilder. offset_++; + trailing_spaces_collapsed_ = true; // Make the last item breakable after, even if it was nowrap. - DCHECK(!item_results->IsEmpty()); - item_results->back().can_break_after = true; + 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()); + trailing_spaces_collapsed_ = true; unsigned end = offset_; while (end < item.EndOffset() && text[end] == kSpaceCharacter) end++; - if (end == offset_) - return LineBreakState::kDone; + if (end == offset_) { + state_ = LineBreakState::kDone; + return; + } - NGInlineItemResult* item_result = AddItem(item, end, item_results); + NGInlineItemResult* item_result = AddItem(item, end); 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->shape_result = item.TextShapeResult(); + if (item_result->start_offset == item.StartOffset() && + item_result->end_offset == item.EndOffset()) + item_result->inline_size = item_result->shape_result->SnappedWidth(); + else + UpdateShapeResult(item_result); + position_ += item_result->inline_size; item_result->can_break_after = end < text.length() && !IsBreakableSpace(text[end]); offset_ = end; @@ -453,101 +562,143 @@ NGLineBreaker::LineBreakState NGLineBreaker::HandleTrailingSpaces( // 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; + if (offset_ < item.EndOffset()) { + state_ = LineBreakState::kDone; + return; + } item_index_++; - return LineBreakState::kTrailing; + state_ = LineBreakState::kTrailing; } // Remove trailing collapsible spaces in |line_info|. // https://drafts.csswg.org/css-text-3/#white-space-phase-2 -void NGLineBreaker::RemoveTrailingCollapsibleSpace(NGLineInfo* line_info) { - NGInlineItemResults* item_results = &line_info->Results(); - if (item_results->IsEmpty()) +void NGLineBreaker::RemoveTrailingCollapsibleSpace() { + DCHECK(!trailing_spaces_collapsed_); + + ComputeTrailingCollapsibleSpace(); + trailing_spaces_collapsed_ = true; + if (!trailing_collapsible_space_.has_value()) return; - for (auto it = item_results->rbegin(); it != item_results->rend(); ++it) { + + // We have a trailing collapsible space. Remove it. + NGInlineItemResult* item_result = trailing_collapsible_space_->item_result; + position_ -= item_result->inline_size; + if (scoped_refptr<const ShapeResult>& collapsed_shape_result = + trailing_collapsible_space_->collapsed_shape_result) { + DCHECK_GE(item_result->end_offset, item_result->start_offset + 2); + --item_result->end_offset; + item_result->shape_result = std::move(collapsed_shape_result); + item_result->inline_size = item_result->shape_result->SnappedWidth(); + position_ += item_result->inline_size; + } else { + item_results_->erase(item_result); + } + trailing_collapsible_space_.reset(); +} + +// Compute the width of trailing spaces without removing it. +LayoutUnit NGLineBreaker::TrailingCollapsibleSpaceWidth() { + if (trailing_spaces_collapsed_) + return LayoutUnit(); + + ComputeTrailingCollapsibleSpace(); + if (!trailing_collapsible_space_.has_value()) + return LayoutUnit(); + + // Normally, the width of new_reuslt is smaller, but technically it can be + // larger. In such case, it means the trailing spaces has negative width. + NGInlineItemResult* item_result = trailing_collapsible_space_->item_result; + if (scoped_refptr<const ShapeResult>& collapsed_shape_result = + trailing_collapsible_space_->collapsed_shape_result) { + return item_result->inline_size - collapsed_shape_result->SnappedWidth(); + } + return item_result->inline_size; +} + +// Find trailing collapsible space if exists. The result is cached to +// |trailing_collapsible_space_|. +void NGLineBreaker::ComputeTrailingCollapsibleSpace() { + DCHECK(!trailing_spaces_collapsed_); + + 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; + break; + DCHECK_GT(item_result.end_offset, 0u); DCHECK(item.Style()); - if (!item.Style()->CollapseWhiteSpace()) - return; + if (Text()[item_result.end_offset - 1] != kSpaceCharacter || + !item.Style()->CollapseWhiteSpace() || + // |shape_result| is nullptr if this is an overflow because BreakText() + // uses kNoResultIfOverflow option. + !item_result.shape_result) + break; - // 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; + if (!trailing_collapsible_space_.has_value() || + trailing_collapsible_space_->item_result != &item_result) { + trailing_collapsible_space_.emplace(); + trailing_collapsible_space_->item_result = &item_result; + if (item_result.end_offset - 1 > item_result.start_offset) { + trailing_collapsible_space_->collapsed_shape_result = + TruncateLineEndResult(item_result, item_result.end_offset - 1); + } } return; } + + trailing_collapsible_space_.reset(); } -void NGLineBreaker::AppendHyphen(const NGInlineItem& item, - NGLineInfo* line_info) { +void NGLineBreaker::AppendHyphen(const NGInlineItem& item) { 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()); + HarfBuzzShaper shaper(hyphen_string); 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); + SetLineEndFragment(builder.ToTextFragment()); } // 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) { +void NGLineBreaker::HandleControlItem(const NGInlineItem& item) { 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()); + NGInlineItemResult* item_result = AddItem(item); + item_result->should_create_line_box = true; item_result->has_only_trailing_spaces = true; - line_.is_after_forced_break = true; - line_info->SetIsLastLine(true); - state = LineBreakState::kDone; + is_after_forced_break_ = true; + line_info_->SetIsLastLine(true); + state_ = LineBreakState::kDone; break; } case kTabulationCharacter: { - NGInlineItemResult* item_result = AddItem(item, &line_info->Results()); + NGInlineItemResult* item_result = AddItem(item); + item_result->should_create_line_box = true; 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->inline_size = font.TabWidth(style.GetTabSize(), position_); + position_ += item_result->inline_size; item_result->has_only_trailing_spaces = - state == LineBreakState::kTrailing; + 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()); + NGInlineItemResult* item_result = AddItem(item); + item_result->should_create_line_box = true; item_result->can_break_after = true; break; } @@ -562,15 +713,10 @@ NGLineBreaker::LineBreakState NGLineBreaker::HandleControlItem( break; } MoveToNextOf(item); - return state; } -NGLineBreaker::LineBreakState NGLineBreaker::HandleBidiControlItem( - const NGInlineItem& item, - LineBreakState state, - NGLineInfo* line_info) { +void NGLineBreaker::HandleBidiControlItem(const NGInlineItem& item) { 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 @@ -579,34 +725,33 @@ NGLineBreaker::LineBreakState NGLineBreaker::HandleBidiControlItem( 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]; + if (!item_results_->IsEmpty()) { + NGInlineItemResult* item_result = AddItem(item); + 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); + AddItem(item); } } else { - if (state == LineBreakState::kTrailing && - CanBreakAfterLast(*item_results)) { - line_info->SetIsLastLine(false); + if (state_ == LineBreakState::kTrailing && + CanBreakAfterLast(*item_results_)) { + line_info_->SetIsLastLine(false); MoveToNextOf(item); - return LineBreakState::kDone; + state_ = LineBreakState::kDone; + return; } - NGInlineItemResult* item_result = AddItem(item, item_results); + NGInlineItemResult* item_result = AddItem(item); DCHECK(!item_result->can_break_after); } MoveToNextOf(item); - return state; } -void NGLineBreaker::HandleAtomicInline(const NGInlineItem& item, - NGLineInfo* line_info) { +void NGLineBreaker::HandleAtomicInline(const NGInlineItem& item) { DCHECK_EQ(item.Type(), NGInlineItem::kAtomicInline); - line_.should_create_line_box = true; - NGInlineItemResult* item_result = AddItem(item, &line_info->Results()); + NGInlineItemResult* item_result = AddItem(item); + item_result->should_create_line_box = true; // When we're just computing min/max content sizes, we can skip the full // layout and just compute those sizes. On the other hand, for regular // layout we need to do the full layout and get the layout result. @@ -616,8 +761,8 @@ void NGLineBreaker::HandleAtomicInline(const NGInlineItem& item, item_result->layout_result = NGBlockNode(ToLayoutBox(item.GetLayoutObject())) .LayoutAtomicInline(constraint_space_, - line_info->LineStyle().GetFontBaseline(), - line_info->UseFirstLineStyle()); + line_info_->LineStyle().GetFontBaseline(), + line_info_->UseFirstLineStyle()); DCHECK(item_result->layout_result->PhysicalFragment()); item_result->inline_size = @@ -636,12 +781,18 @@ void NGLineBreaker::HandleAtomicInline(const NGInlineItem& item, } DCHECK(item.Style()); + const ComputedStyle& style = *item.Style(); + bool is_flipped_lines = style.IsFlippedLinesWritingMode(); item_result->margins = - ComputeMarginsForVisualContainer(constraint_space_, *item.Style()); - item_result->padding = ComputePadding(constraint_space_, *item.Style()); + NGLineBoxStrut(ComputeMarginsForVisualContainer(constraint_space_, style), + is_flipped_lines); + item_result->padding = NGLineBoxStrut( + ComputePadding(constraint_space_, style), is_flipped_lines); item_result->inline_size += item_result->margins.InlineSum(); - line_.position += item_result->inline_size; + if (state_ == LineBreakState::kLeading) + state_ = LineBreakState::kContinue; + position_ += item_result->inline_size; ComputeCanBreakAfter(item_result); MoveToNextOf(item); } @@ -660,9 +811,7 @@ void NGLineBreaker::HandleAtomicInline(const NGInlineItem& item, // 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) { +void NGLineBreaker::HandleFloat(const NGInlineItem& item) { // When rewind occurs, an item may be handled multiple times. // Since floats are put into a separate list, avoid handling same floats // twice. @@ -672,18 +821,12 @@ void NGLineBreaker::HandleFloat(const NGInlineItem& item, // 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. + NGInlineItemResult* item_result = AddItem(item); 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(); @@ -706,7 +849,21 @@ void NGLineBreaker::HandleFloat(const NGInlineItem& item, margins.InlineSum()) .ClampNegativeToZero(); - LayoutUnit bfc_block_offset = line_.line_opportunity.bfc_block_offset; + LayoutUnit bfc_block_offset = line_opportunity_.bfc_block_offset; + + bool can_fit_float = + position_ + inline_margin_size <= + line_opportunity_.AvailableFloatInlineSize().AddEpsilon(); + if (!can_fit_float) { + // Floats need to know the current line width to determine whether to put it + // into the current line or to the next line. Trailing spaces will be + // removed if this line breaks here because they should be collapsed across + // floats, but they are still included in the current line position at this + // point. Exclude it when computing whether this float can fit or not. + can_fit_float = + position_ + inline_margin_size - TrailingCollapsibleSpaceWidth() <= + line_opportunity_.AvailableFloatInlineSize().AddEpsilon(); + } // The float should be positioned after the current line if: // - It can't fit within the non-shape area. (Assuming the current position @@ -717,7 +874,7 @@ void NGLineBreaker::HandleFloat(const NGInlineItem& item, // unpositioned_floats to manually adjust the min/max-content size after // the line breaker has run). bool float_after_line = - !line_.CanFloatFit(inline_margin_size) || + !can_fit_float || exclusion_space_->LastFloatBlockStart() > bfc_block_offset || exclusion_space_->ClearanceOffset(float_style.Clear()) > bfc_block_offset || @@ -743,11 +900,10 @@ void NGLineBreaker::HandleFloat(const NGInlineItem& item, DCHECK_EQ(bfc_block_offset, opportunity.rect.BlockStartOffset()); - line_.line_opportunity = opportunity.ComputeLineLayoutOpportunity( - constraint_space_, line_.line_opportunity.line_block_size, - LayoutUnit()); + line_opportunity_ = opportunity.ComputeLineLayoutOpportunity( + constraint_space_, line_opportunity_.line_block_size, LayoutUnit()); - DCHECK_GE(line_.AvailableWidth(), LayoutUnit()); + DCHECK_GE(AvailableWidth(), LayoutUnit()); } } @@ -762,38 +918,42 @@ bool NGLineBreaker::ComputeOpenTagResult( 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; + bool is_flipped_lines = style.IsFlippedLinesWritingMode(); + NGLineBoxStrut borders = NGLineBoxStrut( + ComputeBorders(constraint_space, style), is_flipped_lines); + item_result->padding = NGLineBoxStrut( + ComputePadding(constraint_space, style), is_flipped_lines); + item_result->borders_paddings_line_over = + borders.line_over + item_result->padding.line_over; + item_result->borders_paddings_line_under = + borders.line_under + item_result->padding.line_under; if (item_result->has_edge) { - item_result->margins = ComputeMarginsForSelf(constraint_space, style); + item_result->margins = NGLineBoxStrut( + ComputeMarginsForSelf(constraint_space, style), is_flipped_lines); item_result->inline_size = item_result->margins.inline_start + - borders.inline_start + paddings.inline_start; + borders.inline_start + + item_result->padding.inline_start; return true; } } return false; } -void NGLineBreaker::HandleOpenTag(const NGInlineItem& item, - NGInlineItemResult* item_result) { +void NGLineBreaker::HandleOpenTag(const NGInlineItem& item) { + NGInlineItemResult* item_result = AddItem(item); DCHECK(!item_result->can_break_after); if (ComputeOpenTagResult(item, constraint_space_, item_result)) { - line_.position += item_result->inline_size; + 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 && + if (!item_result->should_create_line_box && (item_result->inline_size || (item_result->margins.inline_start && !in_line_height_quirks_mode_))) - line_.should_create_line_box = true; + item_result->should_create_line_box = true; } DCHECK(item.Style()); @@ -802,24 +962,25 @@ void NGLineBreaker::HandleOpenTag(const NGInlineItem& item, MoveToNextOf(item); } -void NGLineBreaker::HandleCloseTag(const NGInlineItem& item, - NGInlineItemResults* item_results) { - NGInlineItemResult* item_result = AddItem(item, item_results); +void NGLineBreaker::HandleCloseTag(const NGInlineItem& item) { + NGInlineItemResult* item_result = AddItem(item); 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); + bool is_flipped_lines = style.IsFlippedLinesWritingMode(); + item_result->margins = NGLineBoxStrut( + ComputeMarginsForSelf(constraint_space_, style), is_flipped_lines); 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; + position_ += item_result->inline_size; - if (!line_.should_create_line_box && + if (!item_result->should_create_line_box && (item_result->inline_size || (item_result->margins.inline_end && !in_line_height_quirks_mode_))) - line_.should_create_line_box = true; + item_result->should_create_line_box = true; } DCHECK(item.GetLayoutObject() && item.GetLayoutObject()->Parent()); bool was_auto_wrap = auto_wrap_; @@ -831,8 +992,8 @@ void NGLineBreaker::HandleCloseTag(const NGInlineItem& item, // 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 (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; @@ -859,31 +1020,26 @@ void NGLineBreaker::HandleCloseTag(const NGInlineItem& item, // 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; +void NGLineBreaker::HandleOverflow() { + LayoutUnit available_width = AvailableWidthToFit(); + LayoutUnit width_to_rewind = position_ - available_width; DCHECK_GT(width_to_rewind, 0); + bool position_maybe_changed = false; // 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]; + 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 (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; + position_ = available_width + width_to_rewind; + Rewind(i + 1); + state_ = LineBreakState::kTrailing; + return; } break_before = i + 1; } @@ -900,33 +1056,32 @@ NGLineBreaker::LineBreakState NGLineBreaker::HandleOverflow( 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); + BreakText(item_result, item, item_available_width); #if DCHECK_IS_ON() - item_result->CheckConsistency(); + item_result->CheckConsistency(true); #endif + // If BreakText() changed this item small enough to fit, break here. 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()) { + 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 = + 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 + if (line_info_->LineEndFragment()) + SetLineEndFragment(nullptr); + DCHECK_EQ(position_, line_info_->ComputeWidth()); item_index_ = item_result->item_index; offset_ = item_result->end_offset; items_data_.AssertOffset(item_index_, offset_); } else { - Rewind(line_info, i + 1); + Rewind(i + 1); } - return LineBreakState::kTrailing; + state_ = LineBreakState::kTrailing; + return; } + position_maybe_changed = true; } width_to_rewind = next_width_to_rewind; @@ -937,65 +1092,81 @@ NGLineBreaker::LineBreakState NGLineBreaker::HandleOverflow( if (break_anywhere_if_overflow_ && !override_break_anywhere_) { override_break_anywhere_ = true; break_iterator_.SetBreakType(LineBreakType::kBreakCharacter); - Rewind(line_info, 0); - return LineBreakState::kContinue; + if (!item_results_->IsEmpty()) + Rewind(0); + state_ = LineBreakState::kLeading; + return; } // 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; + Rewind(break_before); + state_ = LineBreakState::kTrailing; + return; } - return LineBreakState::kTrailing; + if (position_maybe_changed) { + UpdatePosition(); + } + + state_ = LineBreakState::kTrailing; } -void NGLineBreaker::Rewind(NGLineInfo* line_info, unsigned new_end) { - NGInlineItemResults* item_results = &line_info->Results(); - DCHECK_LT(new_end, item_results->size()); +void NGLineBreaker::Rewind(unsigned new_end) { + NGInlineItemResults& item_results = *item_results_; + DCHECK_LT(new_end, item_results.size()); + + // Avoid rewinding floats if possible. They will be added back anyway while + // processing trailing items even when zero available width. Also this saves + // most cases where our support for rewinding positioned floats is not great + // yet (see below.) + while (item_results[new_end].item->Type() == NGInlineItem::kFloating) { + ++new_end; + if (new_end == item_results.size()) { + UpdatePosition(); + return; + } + } - // TODO(ikilpatrick): Add DCHECK that we never rewind past any floats. + // Because floats are added to |positioned_floats_| or |unpositioned_floats_|, + // rewinding them needs to remove from these lists too. + for (unsigned i = item_results.size(); i > new_end;) { + NGInlineItemResult& rewind = item_results[--i]; + if (rewind.item->Type() == NGInlineItem::kFloating) { + NGBlockNode float_node(ToLayoutBox(rewind.item->GetLayoutObject())); + if (!RemoveUnpositionedFloat(unpositioned_floats_, float_node)) { + // TODO(kojii): We do not have mechanism to remove once positioned + // floats yet, and that rewinding them may lay it out twice. For now, + // prohibit rewinding positioned floats. This may results in incorrect + // layout, but still better than rewinding them. + new_end = i + 1; + if (new_end == item_results.size()) { + UpdatePosition(); + return; + } + break; + } + } + } 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]); + MoveToNextOf(item_results[new_end - 1]); } else { // When rewinding all items, use |results[0].start_offset|. - const NGInlineItemResult& first_remove = (*item_results)[new_end]; + 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()); -} + item_results.Shrink(new_end); -// 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; + trailing_spaces_collapsed_ = false; + trailing_collapsible_space_.reset(); + SetLineEndFragment(nullptr); + UpdatePosition(); } void NGLineBreaker::SetCurrentStyle(const ComputedStyle& style) { @@ -1058,7 +1229,7 @@ scoped_refptr<NGInlineBreakToken> NGLineBreaker::CreateBreakToken( return NGInlineBreakToken::Create(node_); return NGInlineBreakToken::Create( node_, current_style_.get(), item_index_, offset_, - ((line_.is_after_forced_break ? NGInlineBreakToken::kIsForcedBreak : 0) | + ((is_after_forced_break_ ? NGInlineBreakToken::kIsForcedBreak : 0) | (line_info.UseFirstLineStyle() ? NGInlineBreakToken::kUseFirstLineStyle : 0)), std::move(state_stack)); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.h index b0838c40a67..5f52e8c952e 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.h @@ -5,11 +5,12 @@ #ifndef NGLineBreaker_h #define NGLineBreaker_h +#include "base/optional.h" #include "third_party/blink/renderer/core/core_export.h" #include "third_party/blink/renderer/core/layout/ng/exclusions/ng_line_layout_opportunity.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_item_result.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h" -#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h" +#include "third_party/blink/renderer/platform/fonts/shaping/harfbuzz_shaper.h" #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h" #include "third_party/blink/renderer/platform/text/text_break_iterator.h" #include "third_party/blink/renderer/platform/wtf/allocator.h" @@ -43,12 +44,13 @@ class CORE_EXPORT NGLineBreaker { NGContainerFragmentBuilder* container_builder, NGExclusionSpace*, unsigned handled_float_index, + const NGLineLayoutOpportunity&, const NGInlineBreakToken* = nullptr); ~NGLineBreaker(); // Compute the next line break point and produces NGInlineItemResults for // the line. - bool NextLine(const NGLineLayoutOpportunity& line_opportunity, NGLineInfo*); + void NextLine(NGLineInfo*); // Create an NGInlineBreakToken for the last line returned by NextLine(). scoped_refptr<NGInlineBreakToken> CreateBreakToken( @@ -62,63 +64,20 @@ class CORE_EXPORT NGLineBreaker { NGInlineItemResult*); private: - // This struct holds information for the current line. - struct LineData { - STACK_ALLOCATED(); - - LineData(NGInlineNode node, const NGInlineBreakToken* break_token); - - // The current position from inline_start. Unlike NGInlineLayoutAlgorithm - // that computes position in visual order, this position in logical order. - LayoutUnit position; - - NGLineLayoutOpportunity line_opportunity; - - // True if this line is the "first formatted line". - // https://www.w3.org/TR/CSS22/selector.html#first-formatted-line - bool is_first_formatted_line; - - bool use_first_line_style; - - // We don't create "certain zero-height line boxes". - // https://drafts.csswg.org/css2/visuren.html#phantom-line-box - // Such line boxes do not prevent two margins being "adjoining", and thus - // collapsing. - // https://drafts.csswg.org/css2/box.html#collapsing-margins - bool should_create_line_box = false; - - // Set when the line ended with a forced break. Used to setup the states for - // the next line. - bool is_after_forced_break = false; - - LayoutUnit AvailableWidth() const { - return line_opportunity.AvailableInlineSize(); - } - bool CanFit() const { return position <= AvailableWidth(); } - bool CanFit(LayoutUnit extra) const { - return position + extra <= AvailableWidth(); - } - bool CanFloatFit(LayoutUnit extra) const { - return position + extra <= line_opportunity.AvailableFloatInlineSize(); - } - }; - const String& Text() const { return items_data_.text_content; } const Vector<NGInlineItem>& Items() const { return items_data_.items; } - NGInlineItemResult* AddItem(const NGInlineItem&, - unsigned end_offset, - NGInlineItemResults*); - NGInlineItemResult* AddItem(const NGInlineItem&, NGInlineItemResults*); - void SetLineEndFragment(scoped_refptr<NGPhysicalTextFragment>, NGLineInfo*); + NGInlineItemResult* AddItem(const NGInlineItem&, unsigned end_offset); + NGInlineItemResult* AddItem(const NGInlineItem&); + void SetLineEndFragment(scoped_refptr<NGPhysicalTextFragment>); void ComputeCanBreakAfter(NGInlineItemResult*) const; - void BreakLine(NGLineInfo*); + void BreakLine(); - void PrepareNextLine(const NGLineLayoutOpportunity&, NGLineInfo*); + void PrepareNextLine(); - void UpdatePosition(const NGInlineItemResults&); - void ComputeLineLocation(NGLineInfo*) const; + void UpdatePosition(); + void ComputeLineLocation() const; enum class LineBreakState { // The line breaking is complete. @@ -129,72 +88,81 @@ class CORE_EXPORT NGLineBreaker { // when it is overflowing. kTrailing, - // The initial state. Looking for items to break the line. + // The initial state, until the first character is found. + kLeading, + + // Looking for more items to fit into the current line. kContinue, }; - LineBreakState HandleText(const NGInlineItem&, LineBreakState, NGLineInfo*); + void HandleText(const NGInlineItem&); void BreakText(NGInlineItemResult*, const NGInlineItem&, - LayoutUnit available_width, - NGLineInfo*); - LineBreakState HandleTrailingSpaces(const NGInlineItem&, NGLineInfo*); - void RemoveTrailingCollapsibleSpace(NGLineInfo*); - void AppendHyphen(const NGInlineItem& item, NGLineInfo*); - - LineBreakState HandleControlItem(const NGInlineItem&, - LineBreakState, - NGLineInfo*); - LineBreakState HandleBidiControlItem(const NGInlineItem&, - LineBreakState, - NGLineInfo*); - void HandleAtomicInline(const NGInlineItem&, NGLineInfo*); - void HandleFloat(const NGInlineItem&, NGLineInfo*, NGInlineItemResult*); - - void HandleOpenTag(const NGInlineItem&, NGInlineItemResult*); - void HandleCloseTag(const NGInlineItem&, NGInlineItemResults*); - - LineBreakState HandleOverflow(NGLineInfo*); - LineBreakState HandleOverflow(NGLineInfo*, LayoutUnit available_width); - void Rewind(NGLineInfo*, unsigned new_end); - - LayoutObject* CurrentLayoutObject(const NGLineInfo&) const; + LayoutUnit available_width); + + scoped_refptr<ShapeResult> TruncateLineEndResult( + const NGInlineItemResult& item_result, + unsigned end_offset); + void UpdateShapeResult(NGInlineItemResult*); + scoped_refptr<ShapeResult> ShapeText(const NGInlineItem& item, + unsigned start, + unsigned end); + + void HandleTrailingSpaces(const NGInlineItem&); + void RemoveTrailingCollapsibleSpace(); + LayoutUnit TrailingCollapsibleSpaceWidth(); + void ComputeTrailingCollapsibleSpace(); + + void AppendHyphen(const NGInlineItem& item); + + void HandleControlItem(const NGInlineItem&); + void HandleBidiControlItem(const NGInlineItem&); + void HandleAtomicInline(const NGInlineItem&); + void HandleFloat(const NGInlineItem&); + + void HandleOpenTag(const NGInlineItem&); + void HandleCloseTag(const NGInlineItem&); + + void HandleOverflow(); + void Rewind(unsigned new_end); void SetCurrentStyle(const ComputedStyle&); void MoveToNextOf(const NGInlineItem&); void MoveToNextOf(const NGInlineItemResult&); - void ComputeBaseDirection(const NGLineInfo&); - bool IsTrailing(const NGInlineItem&, const NGLineInfo&) const; + void ComputeBaseDirection(); - LineData line_; - NGInlineNode node_; - const NGInlineItemsData& items_data_; + LayoutUnit AvailableWidth() const { + return line_opportunity_.AvailableInlineSize(); + } + LayoutUnit AvailableWidthToFit() const { + return AvailableWidth().AddEpsilon(); + } - NGLineBreakerMode mode_; - const NGConstraintSpace& constraint_space_; - Vector<NGPositionedFloat>* positioned_floats_; - Vector<scoped_refptr<NGUnpositionedFloat>>* unpositioned_floats_; - NGContainerFragmentBuilder* container_builder_; /* May be nullptr */ - NGExclusionSpace* exclusion_space_; - scoped_refptr<const ComputedStyle> current_style_; + // These fields are the output of the current line. + // NGInlineItemResults is a pointer because the move operation is not cheap + // due to its inline buffer. + NGLineInfo* line_info_ = nullptr; + NGInlineItemResults* item_results_ = nullptr; + // Represents the current offset of the input. + LineBreakState state_; unsigned item_index_ = 0; unsigned offset_ = 0; - LazyLineBreakIterator break_iterator_; - HarfBuzzShaper shaper_; - ShapeResultSpacing<String> spacing_; - bool previous_line_had_forced_break_ = false; - const Hyphenation* hyphenation_ = nullptr; - // Keep track of handled float items. See HandleFloat(). - unsigned handled_floats_end_item_index_; + // The current position from inline_start. Unlike NGInlineLayoutAlgorithm + // that computes position in visual order, this position in logical order. + LayoutUnit position_; + NGLineLayoutOpportunity line_opportunity_; - // The current base direction for the bidi algorithm. - // This is copied from NGInlineNode, then updated after each forced line break - // if 'unicode-bidi: plaintext'. - TextDirection base_direction_; + NGInlineNode node_; + + // True if this line is the "first formatted line". + // https://www.w3.org/TR/CSS22/selector.html#first-formatted-line + bool is_first_formatted_line_ = false; + + bool use_first_line_style_ = false; // True when current box allows line wrapping. bool auto_wrap_ = false; @@ -214,7 +182,49 @@ class CORE_EXPORT NGLineBreaker { // https://quirks.spec.whatwg.org/#the-line-height-calculation-quirk bool in_line_height_quirks_mode_ = false; + // True when the line we are breaking has a list marker. + bool has_list_marker_ = false; + + // True if trailing collapsible spaces have been collapsed. + bool trailing_spaces_collapsed_ = false; + + // Set when the line ended with a forced break. Used to setup the states for + // the next line. + bool is_after_forced_break_ = false; + bool ignore_floats_ = false; + + const NGInlineItemsData& items_data_; + + NGLineBreakerMode mode_; + const NGConstraintSpace& constraint_space_; + Vector<NGPositionedFloat>* positioned_floats_; + Vector<scoped_refptr<NGUnpositionedFloat>>* unpositioned_floats_; + NGContainerFragmentBuilder* container_builder_; /* May be nullptr */ + NGExclusionSpace* exclusion_space_; + scoped_refptr<const ComputedStyle> current_style_; + + LazyLineBreakIterator break_iterator_; + HarfBuzzShaper shaper_; + ShapeResultSpacing<String> spacing_; + bool previous_line_had_forced_break_ = false; + const Hyphenation* hyphenation_ = nullptr; + + // Cache the result of |ComputeTrailingCollapsibleSpace| to avoid shaping + // multiple times. + struct TrailingCollapsibleSpace { + NGInlineItemResult* item_result; + scoped_refptr<const ShapeResult> collapsed_shape_result; + }; + base::Optional<TrailingCollapsibleSpace> trailing_collapsible_space_; + + // Keep track of handled float items. See HandleFloat(). + unsigned handled_floats_end_item_index_; + + // The current base direction for the bidi algorithm. + // This is copied from NGInlineNode, then updated after each forced line break + // if 'unicode-bidi: plaintext'. + TextDirection base_direction_; }; } // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker_test.cc index 30c5402b812..1a6baf2a451 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker_test.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker_test.cc @@ -48,13 +48,16 @@ class NGLineBreakerTest : public NGBaseLayoutAlgorithmTest { Vector<NGInlineItemResults> lines; NGExclusionSpace exclusion_space; NGLineLayoutOpportunity line_opportunity(available_width); - NGLineInfo line_info; while (!break_token || !break_token->IsFinished()) { + NGLineInfo line_info; NGLineBreaker line_breaker(node, NGLineBreakerMode::kContent, *space, &positioned_floats, &unpositioned_floats, /* container_builder */ nullptr, - &exclusion_space, 0u, break_token.get()); - if (!line_breaker.NextLine(line_opportunity, &line_info)) + &exclusion_space, 0u, line_opportunity, + break_token.get()); + line_breaker.NextLine(&line_info); + + if (line_info.Results().IsEmpty()) break; break_token = line_breaker.CreateBreakToken(line_info, nullptr); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.cc index b83d812d685..7b773abd116 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.cc @@ -25,8 +25,10 @@ NGLineHeightMetrics::NGLineHeightMetrics(const FontMetrics& font_metrics, void NGLineHeightMetrics::Initialize(const FontMetrics& font_metrics, FontBaseline baseline_type) { - ascent = font_metrics.FixedAscent(baseline_type); - descent = font_metrics.FixedDescent(baseline_type); + // TODO(kojii): In future, we'd like to use LayoutUnit metrics to support + // sub-CSS-pixel layout. + ascent = LayoutUnit(font_metrics.Ascent(baseline_type)); + descent = LayoutUnit(font_metrics.Descent(baseline_type)); } void NGLineHeightMetrics::AddLeading(LayoutUnit line_height) { @@ -49,4 +51,12 @@ void NGLineHeightMetrics::Unite(const NGLineHeightMetrics& other) { descent = std::max(descent, other.descent); } +void NGLineHeightMetrics::operator+=(const NGLineHeightMetrics& other) { + DCHECK(ascent != LayoutUnit::Min() && descent != LayoutUnit::Min()); + DCHECK(other.ascent != LayoutUnit::Min() && + other.descent != LayoutUnit::Min()); + ascent += other.ascent; + descent += other.descent; +} + } // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.h index 978c170044e..e5ef21b8580 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.h @@ -33,6 +33,13 @@ struct NGLineHeightMetrics { bool IsEmpty() const { return ascent == LayoutUnit::Min(); } + bool operator==(const NGLineHeightMetrics& other) const { + return ascent == other.ascent && descent == other.descent; + } + bool operator!=(const NGLineHeightMetrics& other) const { + return !operator==(other); + } + // Add the leading. Half the leading is added to ascent and descent each. // https://drafts.csswg.org/css2/visudet.html#leading void AddLeading(LayoutUnit line_height); @@ -43,6 +50,8 @@ struct NGLineHeightMetrics { // Unite a metrics for an inline box to a metrics for a line box. void Unite(const NGLineHeightMetrics&); + void operator+=(const NGLineHeightMetrics&); + // Ascent and descent of glyphs, or synthesized for replaced elements. // Then united to compute 'text-top' and 'text-bottom' of line boxes. LayoutUnit ascent; diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_truncator.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_truncator.cc index ae76600c718..919cb689b26 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_truncator.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_truncator.cc @@ -7,7 +7,7 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_item_result.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.h" #include "third_party/blink/renderer/platform/fonts/font_baseline.h" -#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h" +#include "third_party/blink/renderer/platform/fonts/shaping/harfbuzz_shaper.h" namespace blink { @@ -54,7 +54,7 @@ LayoutUnit NGLineTruncator::TruncateLine( font_data && font_data->GlyphForCharacter(kHorizontalEllipsisCharacter) ? String(&kHorizontalEllipsisCharacter, 1) : String(u"..."); - HarfBuzzShaper shaper(ellipsis_text.Characters16(), ellipsis_text.length()); + HarfBuzzShaper shaper(ellipsis_text); scoped_refptr<ShapeResult> ellipsis_shape_result = shaper.Shape(&font, line_direction_); LayoutUnit ellipsis_width = ellipsis_shape_result->SnappedWidth(); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.cc index e341ddffdae..3187df368dd 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.cc @@ -5,48 +5,17 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.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_physical_text_fragment.h" +#include "third_party/blink/renderer/core/layout/ng/ng_relative_utils.h" #include "third_party/blink/renderer/core/style/computed_style.h" namespace blink { -namespace { - -static const NGPhysicalFragment* LastLogicalLeafExceptLinebreakInternal( - const NGPhysicalFragment& runner, - TextDirection direction) { - if (runner.IsText()) { - if (ToNGPhysicalTextFragment(runner).IsLineBreak()) - return nullptr; - return &runner; - } - if (!runner.IsContainer() || runner.IsBlockLayoutRoot()) - return &runner; - const auto& children = ToNGPhysicalContainerFragment(runner).Children(); - for (size_t i = 0; i < children.size(); i++) { - // TODO(xiaochengh): This isn't correct for mixed Bidi. Fix it. Besides, we - // should compute and store it during layout. - // We want a logical last child in a line. - const size_t index = - direction == TextDirection::kLtr ? (children.size() - 1 - i) : i; - const NGPhysicalFragment* child = children[index].get(); - DCHECK(child); - if (const NGPhysicalFragment* candidate = - LastLogicalLeafExceptLinebreakInternal(*child, direction)) - return candidate; - } - return nullptr; -} - -} // namespace - NGPhysicalLineBoxFragment::NGPhysicalLineBoxFragment( const ComputedStyle& style, NGStyleVariant style_variant, NGPhysicalSize size, Vector<scoped_refptr<NGPhysicalFragment>>& children, - const NGPhysicalOffsetRect& contents_visual_rect, - const NGPhysicalOffsetRect& scrollable_overflow, + const NGPhysicalOffsetRect& contents_ink_overflow, const NGLineHeightMetrics& metrics, TextDirection base_direction, scoped_refptr<NGBreakToken> break_token) @@ -57,22 +26,43 @@ NGPhysicalLineBoxFragment::NGPhysicalLineBoxFragment( kFragmentLineBox, 0, children, - contents_visual_rect, + contents_ink_overflow, std::move(break_token)), - scrollable_overflow_(scrollable_overflow), metrics_(metrics) { base_direction_ = static_cast<unsigned>(base_direction); } -LayoutUnit NGPhysicalLineBoxFragment::BaselinePosition(FontBaseline) const { +NGLineHeightMetrics NGPhysicalLineBoxFragment::BaselineMetrics( + FontBaseline) const { // TODO(kojii): Computing other baseline types than the used one is not // implemented yet. // TODO(kojii): We might need locale/script to look up OpenType BASE table. - return metrics_.ascent; + return metrics_; } -NGPhysicalOffsetRect NGPhysicalLineBoxFragment::VisualRectWithContents() const { - return ContentsVisualRect(); +NGPhysicalOffsetRect NGPhysicalLineBoxFragment::InkOverflow() const { + return ContentsInkOverflow(); +} + +NGPhysicalOffsetRect NGPhysicalLineBoxFragment::ScrollableOverflow( + const ComputedStyle* container_style, + NGPhysicalSize container_physical_size) const { + WritingMode container_writing_mode = container_style->GetWritingMode(); + TextDirection container_direction = container_style->Direction(); + NGPhysicalOffsetRect overflow({}, Size()); + for (const auto& child : Children()) { + NGPhysicalOffsetRect child_scroll_overflow = child->ScrollableOverflow(); + child_scroll_overflow.offset += child->Offset(); + // If child has the same style as parent, parent will compute relative + // offset. + if (&child->Style() != container_style) { + child_scroll_overflow.offset += + ComputeRelativeOffset(child->Style(), container_writing_mode, + container_direction, container_physical_size); + } + overflow.Unite(child_scroll_overflow); + } + return overflow; } const NGPhysicalFragment* NGPhysicalLineBoxFragment::FirstLogicalLeaf() const { @@ -115,13 +105,6 @@ const NGPhysicalFragment* NGPhysicalLineBoxFragment::LastLogicalLeaf() const { return runner; } -const NGPhysicalFragment* -NGPhysicalLineBoxFragment::LastLogicalLeafIgnoringLineBreak() const { - if (Children().IsEmpty()) - return nullptr; - return LastLogicalLeafExceptLinebreakInternal(*this, this->BaseDirection()); -} - bool NGPhysicalLineBoxFragment::HasSoftWrapToNextLine() const { DCHECK(BreakToken()); DCHECK(BreakToken()->IsInlineType()); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h index b92b910d2b3..7ca454dcb6b 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h @@ -20,8 +20,7 @@ class CORE_EXPORT NGPhysicalLineBoxFragment final NGStyleVariant style_variant, NGPhysicalSize size, Vector<scoped_refptr<NGPhysicalFragment>>& children, - const NGPhysicalOffsetRect& contents_visual_rect, - const NGPhysicalOffsetRect& scrollable_overflow, + const NGPhysicalOffsetRect& contents_ink_overflow, const NGLineHeightMetrics&, TextDirection base_direction, scoped_refptr<NGBreakToken> break_token = nullptr); @@ -36,23 +35,23 @@ class CORE_EXPORT NGPhysicalLineBoxFragment final } // Compute baseline for the specified baseline type. - LayoutUnit BaselinePosition(FontBaseline) const; + NGLineHeightMetrics BaselineMetrics(FontBaseline) const; - // VisualRect of itself including contents, in the local coordinate. - NGPhysicalOffsetRect VisualRectWithContents() const; + // Ink overflow of itself including contents, in the local coordinate. + NGPhysicalOffsetRect InkOverflow() const; // Scrollable overflow. including contents, in the local coordinate. - NGPhysicalOffsetRect ScrollableOverflow() const { - return scrollable_overflow_; - } + // ScrollableOverflow is not precomputed/cached because it cannot be computed + // when LineBox is generated because it needs container dimensions to + // resolve relative position of its children. + NGPhysicalOffsetRect ScrollableOverflow( + const ComputedStyle* container_style, + NGPhysicalSize container_physical_size) const; // Returns the first/last leaf fragment in the line in logical order. Returns // nullptr if the line box is empty. const NGPhysicalFragment* FirstLogicalLeaf() const; const NGPhysicalFragment* LastLogicalLeaf() const; - // Returns the last leaf fragment in the line in logical order except line - // break. Returns nullptr if such fragment doesn't exist. - const NGPhysicalFragment* LastLogicalLeafIgnoringLineBreak() const; // Whether the content soft-wraps to the next line. bool HasSoftWrapToNextLine() const; @@ -60,12 +59,11 @@ class CORE_EXPORT NGPhysicalLineBoxFragment final scoped_refptr<NGPhysicalFragment> CloneWithoutOffset() const { Vector<scoped_refptr<NGPhysicalFragment>> children_copy(children_); return base::AdoptRef(new NGPhysicalLineBoxFragment( - Style(), StyleVariant(), size_, children_copy, contents_visual_rect_, - scrollable_overflow_, metrics_, BaseDirection(), break_token_)); + Style(), StyleVariant(), size_, children_copy, contents_ink_overflow_, + metrics_, BaseDirection(), break_token_)); } private: - NGPhysicalOffsetRect scrollable_overflow_; NGLineHeightMetrics metrics_; }; diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment_test.cc index 331fb87d70b..cd44fd8ea05 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment_test.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment_test.cc @@ -59,7 +59,6 @@ TEST_F(NGPhysicalLineBoxFragmentTest, FirstLastLogicalLeafInSimpleText) { "</div>"); EXPECT_TEXT_FRAGMENT("foo", GetLineBox()->FirstLogicalLeaf()); EXPECT_TEXT_FRAGMENT("bar", GetLineBox()->LastLogicalLeaf()); - EXPECT_TEXT_FRAGMENT("bar", GetLineBox()->LastLogicalLeafIgnoringLineBreak()); } TEST_F(NGPhysicalLineBoxFragmentTest, FirstLastLogicalLeafInRtlText) { @@ -70,7 +69,6 @@ TEST_F(NGPhysicalLineBoxFragmentTest, FirstLastLogicalLeafInRtlText) { "</bdo>"); EXPECT_TEXT_FRAGMENT("foo", GetLineBox()->FirstLogicalLeaf()); EXPECT_TEXT_FRAGMENT("bar", GetLineBox()->LastLogicalLeaf()); - EXPECT_TEXT_FRAGMENT("bar", GetLineBox()->LastLogicalLeafIgnoringLineBreak()); } TEST_F(NGPhysicalLineBoxFragmentTest, @@ -83,7 +81,6 @@ TEST_F(NGPhysicalLineBoxFragmentTest, "</div>"); EXPECT_TEXT_FRAGMENT("f", GetLineBox()->FirstLogicalLeaf()); EXPECT_TEXT_FRAGMENT("r", GetLineBox()->LastLogicalLeaf()); - EXPECT_TEXT_FRAGMENT("r", GetLineBox()->LastLogicalLeafIgnoringLineBreak()); } TEST_F(NGPhysicalLineBoxFragmentTest, FirstLastLogicalLeafWithInlineBlock) { @@ -95,26 +92,12 @@ TEST_F(NGPhysicalLineBoxFragmentTest, FirstLastLogicalLeafWithInlineBlock) { "</div>"); EXPECT_BOX_FRAGMENT("foo", GetLineBox()->FirstLogicalLeaf()); EXPECT_BOX_FRAGMENT("baz", GetLineBox()->LastLogicalLeaf()); - EXPECT_BOX_FRAGMENT("baz", GetLineBox()->LastLogicalLeafIgnoringLineBreak()); } TEST_F(NGPhysicalLineBoxFragmentTest, FirstLastLogicalLeafWithImages) { SetBodyInnerHTML("<div id=root><img id=img1>foo<img id=img2></div>"); EXPECT_BOX_FRAGMENT("img1", GetLineBox()->FirstLogicalLeaf()); EXPECT_BOX_FRAGMENT("img2", GetLineBox()->LastLogicalLeaf()); - EXPECT_BOX_FRAGMENT("img2", GetLineBox()->LastLogicalLeafIgnoringLineBreak()); -} - -TEST_F(NGPhysicalLineBoxFragmentTest, LastLogicalLeafSoftWrap) { - SetBodyInnerHTML("<div id=root style='width: 2em'>foo bar</div>"); - EXPECT_TEXT_FRAGMENT("foo", GetLineBox()->LastLogicalLeaf()); - EXPECT_TEXT_FRAGMENT("foo", GetLineBox()->LastLogicalLeafIgnoringLineBreak()); -} - -TEST_F(NGPhysicalLineBoxFragmentTest, LastLogicalLeafHardWrap) { - SetBodyInnerHTML("<div id=root>foo<br>bar</div>"); - EXPECT_TEXT_FRAGMENT("\n", GetLineBox()->LastLogicalLeaf()); - EXPECT_TEXT_FRAGMENT("foo", GetLineBox()->LastLogicalLeafIgnoringLineBreak()); } } // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.cc index d75f591cc0f..7c705969eb7 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.cc @@ -58,24 +58,31 @@ LayoutUnit NGPhysicalTextFragment::InlinePositionForOffset( AdjustMidCluster::kToEnd); } -NGPhysicalOffsetRect NGPhysicalTextFragment::LocalRect( - unsigned start_offset, - unsigned end_offset) const { +std::pair<LayoutUnit, LayoutUnit> +NGPhysicalTextFragment::LineLeftAndRightForOffsets(unsigned start_offset, + unsigned end_offset) const { DCHECK_LE(start_offset, end_offset); DCHECK_GE(start_offset, start_offset_); DCHECK_LE(end_offset, end_offset_); - LayoutUnit start_position = InlinePositionForOffset( + const LayoutUnit start_position = InlinePositionForOffset( start_offset, LayoutUnit::FromFloatFloor, AdjustMidCluster::kToStart); - LayoutUnit end_position = InlinePositionForOffset( + const LayoutUnit end_position = InlinePositionForOffset( end_offset, LayoutUnit::FromFloatCeil, AdjustMidCluster::kToEnd); // Swap positions if RTL. - if (UNLIKELY(start_position > end_position)) - std::swap(start_position, end_position); - - LayoutUnit inline_size = end_position - start_position; + return (UNLIKELY(start_position > end_position)) + ? std::make_pair(end_position, start_position) + : std::make_pair(start_position, end_position); +} +NGPhysicalOffsetRect NGPhysicalTextFragment::LocalRect( + unsigned start_offset, + unsigned end_offset) const { + LayoutUnit start_position, end_position; + std::tie(start_position, end_position) = + LineLeftAndRightForOffsets(start_offset, end_offset); + const LayoutUnit inline_size = end_position - start_position; switch (LineOrientation()) { case NGLineOrientation::kHorizontal: return {{start_position, LayoutUnit()}, {inline_size, Size().height}}; @@ -89,23 +96,24 @@ NGPhysicalOffsetRect NGPhysicalTextFragment::LocalRect( return {}; } -NGPhysicalOffsetRect NGPhysicalTextFragment::SelfVisualRect() const { +NGPhysicalOffsetRect NGPhysicalTextFragment::SelfInkOverflow() const { if (UNLIKELY(!shape_result_)) return LocalRect(); // Glyph bounds is in logical coordinate, origin at the alphabetic baseline. - LayoutRect visual_rect = EnclosingLayoutRect(shape_result_->Bounds()); + LayoutRect ink_overflow = EnclosingLayoutRect(shape_result_->Bounds()); // Make the origin at the logical top of this fragment. const ComputedStyle& style = Style(); const Font& font = style.GetFont(); if (const SimpleFontData* font_data = font.PrimaryFont()) { - visual_rect.SetY(visual_rect.Y() + font_data->GetFontMetrics().FixedAscent( - kAlphabeticBaseline)); + ink_overflow.SetY( + ink_overflow.Y() + + font_data->GetFontMetrics().FixedAscent(kAlphabeticBaseline)); } if (float stroke_width = style.TextStrokeWidth()) { - visual_rect.Inflate(LayoutUnit::FromFloatCeil(stroke_width / 2.0f)); + ink_overflow.Inflate(LayoutUnit::FromFloatCeil(stroke_width / 2.0f)); } if (style.GetTextEmphasisMark() != TextEmphasisMark::kNone) { @@ -113,13 +121,13 @@ NGPhysicalOffsetRect NGPhysicalTextFragment::SelfVisualRect() const { LayoutUnit(font.EmphasisMarkHeight(style.TextEmphasisMarkString())); DCHECK_GT(emphasis_mark_height, LayoutUnit()); if (style.GetTextEmphasisLineLogicalSide() == LineLogicalSide::kOver) { - visual_rect.ShiftYEdgeTo( - std::min(visual_rect.Y(), -emphasis_mark_height)); + ink_overflow.ShiftYEdgeTo( + std::min(ink_overflow.Y(), -emphasis_mark_height)); } else { LayoutUnit logical_height = style.IsHorizontalWritingMode() ? Size().height : Size().width; - visual_rect.ShiftMaxYEdgeTo( - std::max(visual_rect.MaxY(), logical_height + emphasis_mark_height)); + ink_overflow.ShiftMaxYEdgeTo( + std::max(ink_overflow.MaxY(), logical_height + emphasis_mark_height)); } } @@ -129,16 +137,16 @@ NGPhysicalOffsetRect NGPhysicalTextFragment::SelfVisualRect() const { LayoutRectOutsets(text_shadow->RectOutsetsIncludingOriginal()), style.GetWritingMode()); text_shadow_logical_outsets.ClampNegativeToZero(); - visual_rect.Expand(text_shadow_logical_outsets); + ink_overflow.Expand(text_shadow_logical_outsets); } - visual_rect = LayoutRect(EnclosingIntRect(visual_rect)); + ink_overflow = LayoutRect(EnclosingIntRect(ink_overflow)); // Uniting the frame rect ensures that non-ink spaces such side bearings, or // even space characters, are included in the visual rect for decorations. - NGPhysicalOffsetRect local_visual_rect = ConvertToLocal(visual_rect); - local_visual_rect.Unite(LocalRect()); - return local_visual_rect; + NGPhysicalOffsetRect local_ink_overflow = ConvertToLocal(ink_overflow); + local_ink_overflow.Unite(LocalRect()); + return local_ink_overflow; } scoped_refptr<NGPhysicalFragment> NGPhysicalTextFragment::TrimText( @@ -184,9 +192,9 @@ unsigned NGPhysicalTextFragment::TextOffsetForPoint( DCHECK(TextShapeResult()); const LayoutUnit& point_in_line_direction = Style().IsHorizontalWritingMode() ? point.left : point.top; - const bool include_partial_glyphs = true; return TextShapeResult()->OffsetForPosition(point_in_line_direction.ToFloat(), - include_partial_glyphs) + + IncludePartialGlyphs, + BreakGlyphs) + StartOffset(); } diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h index f6921d10546..19758ce7666 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h @@ -41,11 +41,18 @@ enum class NGLineOrientation { class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { public: enum NGTextType { + // |text_| holds |NGNodeInlineData::text_content_|. kNormalText, kForcedLineBreak, // Flow controls are not to be painted. In particular, a tabulation // character and a soft-wrap opportunity. kFlowControl, + kSymbolMarker, + // |text_| holds generated contents instead of |text_content_| in + // |NGNodeInlineData|, e.g. hyphen, and ellipsis. + // Note: Contents generated by CSS pseudo element, e.g. ::before, ::after, + // are not classified to this. See IsAnonymousText() for them. + kGeneratedText, // When adding new values, make sure the bit size of |sub_type_| is large // enough to store. }; @@ -77,6 +84,8 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { } NGTextType TextType() const { return static_cast<NGTextType>(sub_type_); } + // True if this is a generated text. + bool IsGeneratedText() const { return TextType() == kGeneratedText; } // True if this is a forced line break. bool IsLineBreak() const { return TextType() == kForcedLineBreak; } // True if this is not for painting; i.e., a forced line break, a tabulation, @@ -114,7 +123,7 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { // The visual bounding box that includes glpyh bounding box and CSS // properties, in local coordinates. - NGPhysicalOffsetRect SelfVisualRect() const; + NGPhysicalOffsetRect SelfInkOverflow() const; NGTextEndEffect EndEffect() const { return static_cast<NGTextEndEffect>(end_effect_); @@ -142,6 +151,13 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { UBiDiLevel BidiLevel() const override; TextDirection ResolvedDirection() const override; + // Compute line-relative coordinates for given offsets, this is not + // flow-relative: + // https://drafts.csswg.org/css-writing-modes-3/#line-directions + std::pair<LayoutUnit, LayoutUnit> LineLeftAndRightForOffsets( + unsigned start_offset, + unsigned end_offset) const; + private: LayoutUnit InlinePositionForOffset(unsigned offset, LayoutUnit (*round)(float), diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment_test.cc index 48cc659c404..8962d687d27 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment_test.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment_test.cc @@ -35,6 +35,10 @@ class NGPhysicalTextFragmentTest : public NGLayoutTest { } return result; } + + static std::string GetText(const NGPhysicalTextFragment& fragment) { + return fragment.Text().ToString().Utf8().data(); + } }; TEST_F(NGPhysicalTextFragmentTest, LocalRect) { @@ -155,6 +159,32 @@ TEST_F(NGPhysicalTextFragmentTest, BeforeAndAfterAreAnonymousText) { EXPECT_TRUE(after.IsAnonymousText()); } +TEST_F(NGPhysicalTextFragmentTest, Ellipsis) { + LoadAhem(); + SetBodyInnerHTML(R"HTML( + <style> + #sample { + font: 10px/1 Ahem; + overflow: hidden; + text-overflow: ellipsis; + width: 4ch; + } + </style> + <p id="sample">abcdef</p> + )HTML"); + auto text_fragments = CollectTextFragmentsInContainer("sample"); + ASSERT_EQ(2u, text_fragments.size()); + + const NGPhysicalTextFragment& abcdef = *text_fragments[0]; + const NGPhysicalTextFragment& ellipsis = *text_fragments[1]; + EXPECT_EQ(NGPhysicalTextFragment::kNormalText, abcdef.TextType()); + EXPECT_FALSE(abcdef.IsGeneratedText()); + EXPECT_EQ(u8"abc", GetText(abcdef)); + EXPECT_EQ(NGPhysicalTextFragment::kGeneratedText, ellipsis.TextType()); + EXPECT_TRUE(ellipsis.IsGeneratedText()); + EXPECT_EQ(u8"\u2026", GetText(ellipsis)); +} + TEST_F(NGPhysicalTextFragmentTest, ListMarkerIsAnonymousText) { SetBodyInnerHTML( "<ol style='list-style-position:inside'>" @@ -170,6 +200,37 @@ TEST_F(NGPhysicalTextFragmentTest, ListMarkerIsAnonymousText) { EXPECT_FALSE(text.IsAnonymousText()); } +TEST_F(NGPhysicalTextFragmentTest, SoftHyphen) { + LoadAhem(); + SetBodyInnerHTML(R"HTML( + <style> + #sample { + font: 10px/1 Ahem; + width: 3ch; + } + </style> + <p id="sample">abc­def</p> + )HTML"); + auto text_fragments = CollectTextFragmentsInContainer("sample"); + ASSERT_EQ(3u, text_fragments.size()); + + const NGPhysicalTextFragment& abc = *text_fragments[0]; + const NGPhysicalTextFragment& shy = *text_fragments[1]; + const NGPhysicalTextFragment& def = *text_fragments[2]; + EXPECT_EQ(NGPhysicalTextFragment::kNormalText, abc.TextType()); + EXPECT_FALSE(abc.IsGeneratedText()); + // Note: ShapeResult::RunInfo.width_ == 0 for U+00AD + EXPECT_EQ(u8"abc\u00AD", GetText(abc)); + EXPECT_EQ(NGPhysicalTextFragment::kGeneratedText, shy.TextType()); + EXPECT_TRUE(shy.IsGeneratedText()); + // Note: |ComputedStyle::HypenString()| returns "-" or U+2010 based on + // glyph availability. + if (GetText(shy) != "-") + EXPECT_EQ(u8"\u2010", GetText(shy)); + EXPECT_EQ(NGPhysicalTextFragment::kNormalText, def.TextType()); + EXPECT_FALSE(def.IsGeneratedText()); +} + TEST_F(NGPhysicalTextFragmentTest, QuotationMarksAreAnonymousText) { SetBodyInnerHTML("<div id=div><q>text</q></div>"); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.cc index 9ee319a8d3c..c1cbefd220e 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.cc @@ -40,6 +40,8 @@ void NGTextFragmentBuilder::SetItem( const NGInlineItemsData& items_data, NGInlineItemResult* item_result, LayoutUnit line_height) { + DCHECK_NE(text_type, NGPhysicalTextFragment::kGeneratedText) + << "Please use SetText() instead."; DCHECK(item_result); DCHECK(item_result->item->Style()); @@ -65,7 +67,7 @@ void NGTextFragmentBuilder::SetText( DCHECK(style); DCHECK(shape_result); - text_type_ = NGPhysicalTextFragment::kNormalText; + text_type_ = NGPhysicalTextFragment::kGeneratedText; text_ = text; item_index_ = std::numeric_limits<unsigned>::max(); start_offset_ = shape_result->StartIndexForResult(); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.h index 68efb30de15..df788efc4cb 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.h @@ -29,6 +29,8 @@ class CORE_EXPORT NGTextFragmentBuilder final : public NGBaseFragmentBuilder { const NGInlineItemsData&, NGInlineItemResult*, LayoutUnit line_height); + + // Set text for generated text, e.g. hyphen and ellipsis. void SetText(LayoutObject*, const String& text, scoped_refptr<const ComputedStyle>, |