diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2019-07-31 15:50:41 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2019-08-30 12:35:23 +0000 |
commit | 7b2ffa587235a47d4094787d72f38102089f402a (patch) | |
tree | 30e82af9cbab08a7fa028bb18f4f2987a3f74dfa /chromium/third_party/blink/renderer/core/layout/ng/inline | |
parent | d94af01c90575348c4e81a418257f254b6f8d225 (diff) | |
download | qtwebengine-chromium-7b2ffa587235a47d4094787d72f38102089f402a.tar.gz |
BASELINE: Update Chromium to 76.0.3809.94
Change-Id: I321c3f5f929c105aec0f98c5091ef6108822e647
Reviewed-by: Michael Brüning <michael.bruning@qt.io>
Diffstat (limited to 'chromium/third_party/blink/renderer/core/layout/ng/inline')
48 files changed, 1646 insertions, 1867 deletions
diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/README.md b/chromium/third_party/blink/renderer/core/layout/ng/inline/README.md index c604426d29e..a9b42d7c716 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/README.md +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/README.md @@ -309,11 +309,6 @@ In a bird's‐eye view, it consists of two parts: content of an inline formatting context (computed in [pre-layout]) and DOM positions in the context. See [design doc](https://goo.gl/CJbxky) for details. -[NGCaretNavigator] provides functions for inspecting bidi levels and visual -ordering of text content, and supports visual left/right caret movements in the -text. See [design doc](http://bit.ly/2QVAwGq) for details. - - [ICU BiDi]: http://userguide.icu-project.org/transforms/bidi [UAX#9 Unicode Bidirectional Algorithm]: http://unicode.org/reports/tr9/ [UAX#9 Resolving Embedding Levels]: http://www.unicode.org/reports/tr9/#Resolving_Embedding_Levels @@ -322,7 +317,6 @@ text. See [design doc](http://bit.ly/2QVAwGq) for details. [FontBaseline]: ../../../platform/fonts/FontBaseline.h [NGBaselineAlgorithmType]: ng_baseline.h [NGBaselineRequest]: ng_baseline.h -[NGCaretNavigator]: ng_caret_navigator.h [NGBidiParagraph]: ng_bidi_paragraph.h [NGBlockNode]: ../ng_block_node.h [NGBoxFragment]: ../ng_box_fragment.h diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/layout_ng_text.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/layout_ng_text.h index d922dbf5684..4e2517fba32 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/layout_ng_text.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/layout_ng_text.h @@ -23,12 +23,6 @@ class CORE_EXPORT LayoutNGText : public LayoutText { } bool IsLayoutNGObject() const override { return true; } - protected: - void InsertedIntoTree() override { - valid_ng_items_ = false; - LayoutText::InsertedIntoTree(); - } - private: const NGInlineItems* GetNGInlineItems() const final { return &inline_items_; } NGInlineItems* GetNGInlineItems() final { return &inline_items_; } 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 index 0a93d49372b..7fe1900ab55 100644 --- 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 @@ -5,6 +5,7 @@ #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_inline_break_token.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" @@ -26,12 +27,16 @@ scoped_refptr<AbstractInlineTextBox> NGAbstractInlineTextBox::GetOrCreate( new FragmentToNGAbstractInlineTextBoxHashMap(); } const auto it = g_abstract_inline_text_box_map_->find(&fragment); - if (it != g_abstract_inline_text_box_map_->end()) + LayoutText* const layout_text = + ToLayoutText(fragment.GetMutableLayoutObject()); + if (it != g_abstract_inline_text_box_map_->end()) { + CHECK(layout_text->HasAbstractInlineTextBox()); return it->value; - scoped_refptr<AbstractInlineTextBox> obj = - base::AdoptRef(new NGAbstractInlineTextBox( - LineLayoutText(ToLayoutText(fragment.GetLayoutObject())), fragment)); + } + scoped_refptr<AbstractInlineTextBox> obj = base::AdoptRef( + new NGAbstractInlineTextBox(LineLayoutText(layout_text), fragment)); g_abstract_inline_text_box_map_->Set(&fragment, obj); + layout_text->SetHasAbstractInlineTextBox(); return obj; } @@ -65,12 +70,6 @@ void NGAbstractInlineTextBox::Detach() { fragment_ = nullptr; } -bool NGAbstractInlineTextBox::HasSoftWrapToNextLine() const { - return To<NGPhysicalLineBoxFragment>( - fragment_->ContainerLineBox()->PhysicalFragment()) - .HasSoftWrapToNextLine(); -} - const NGPhysicalTextFragment& NGAbstractInlineTextBox::PhysicalTextFragment() const { return To<NGPhysicalTextFragment>(fragment_->PhysicalFragment()); @@ -81,13 +80,34 @@ bool NGAbstractInlineTextBox::NeedsLayout() const { } bool NGAbstractInlineTextBox::NeedsTrailingSpace() const { - if (!HasSoftWrapToNextLine()) + if (!fragment_->Style().CollapseWhiteSpace()) return false; - const NGPaintFragment* next_fragment = NextTextFragmentForSameLayoutObject(); - if (!next_fragment) + const NGPaintFragment& line_box = *fragment_->ContainerLineBox(); + if (!To<NGPhysicalLineBoxFragment>(line_box.PhysicalFragment()) + .HasSoftWrapToNextLine()) + return false; + const NGPhysicalTextFragment& text_fragment = PhysicalTextFragment(); + if (text_fragment.EndOffset() >= text_fragment.TextContent().length()) + return false; + if (text_fragment.TextContent()[text_fragment.EndOffset()] != ' ') + return false; + const NGInlineBreakToken& break_token = *To<NGInlineBreakToken>( + To<NGPhysicalLineBoxFragment>(line_box.PhysicalFragment()).BreakToken()); + // TODO(yosin): We should support OOF fragments between |fragment_| and + // break token. + if (break_token.TextOffset() != text_fragment.EndOffset() + 1) + return false; + // Check a character in text content after |fragment_| comes from same + // layout text of |fragment_|. + const NGOffsetMapping& mapping = + *NGOffsetMapping::GetFor(fragment_->GetLayoutObject()); + const NGMappingUnitRange& mapping_units = + mapping.GetMappingUnitsForTextContentOffsetRange( + text_fragment.EndOffset(), text_fragment.EndOffset() + 1); + if (mapping_units.begin() == mapping_units.end()) return false; - return To<NGPhysicalTextFragment>(next_fragment->PhysicalFragment()) - .StartOffset() != PhysicalTextFragment().EndOffset(); + const NGOffsetMappingUnit* const mapping_unit = mapping_units.begin(); + return mapping_unit->GetLayoutObject() == fragment_->GetLayoutObject(); } const NGPaintFragment* 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 index d74e962fc41..fb2368ed230 100644 --- 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 @@ -32,7 +32,6 @@ class CORE_EXPORT NGAbstractInlineTextBox final : public AbstractInlineTextBox { NGAbstractInlineTextBox(LineLayoutText line_layout_item, const NGPaintFragment& fragment); - bool HasSoftWrapToNextLine() const; const NGPhysicalTextFragment& PhysicalTextFragment() const; bool NeedsLayout() const; bool NeedsTrailingSpace() const; diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_baseline.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_baseline.h index 22fc93bf751..be83e72efc7 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_baseline.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_baseline.h @@ -149,6 +149,17 @@ class CORE_EXPORT NGBaselineList { void emplace_back(NGBaselineRequest request, LayoutUnit offset); +#if DCHECK_IS_ON() + bool operator==(const NGBaselineList& other) const { + for (wtf_size_t i = 0; i < NGBaselineRequest::kTypeIdCount; ++i) { + if (offsets_[i] != other.offsets_[i]) + return false; + } + + return true; + } +#endif + class const_iterator { public: explicit const_iterator(unsigned type_id, const LayoutUnit* offset) diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_navigator.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_navigator.cc deleted file mode 100644 index 5b2f37f4e13..00000000000 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_navigator.cc +++ /dev/null @@ -1,426 +0,0 @@ -// 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_caret_navigator.h" - -#include "third_party/blink/renderer/core/layout/layout_text_fragment.h" -#include "third_party/blink/renderer/core/layout/ng/inline/ng_bidi_paragraph.h" -#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h" -#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h" -#include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h" -#include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h" - -namespace blink { - -std::ostream& operator<<(std::ostream& out, - const NGCaretNavigator::Position& position) { - return out << position.index << "/" - << (position.IsBeforeCharacter() ? "BeforeCharacter" - : "AfterCharacter"); -} - -NGCaretNavigator::~NGCaretNavigator() = default; - -NGCaretNavigator::NGCaretNavigator(const LayoutBlockFlow& context) - : context_(context), - disallow_transition_(context.GetDocument().Lifecycle()) { - DCHECK(RuntimeEnabledFeatures::BidiCaretAffinityEnabled()); - DCHECK(context.IsLayoutNGMixin()); - DCHECK(context.ChildrenInline()); - DCHECK(context.GetNGInlineNodeData()); - DCHECK(!context.GetDocument().NeedsLayoutTreeUpdate()); -} - -const NGInlineNodeData& NGCaretNavigator::GetData() const { - return *context_.GetNGInlineNodeData(); -} - -const String& NGCaretNavigator::GetText() const { - return GetData().text_content; -} - -bool NGCaretNavigator::IsBidiEnabled() const { - return GetData().IsBidiEnabled(); -} - -UBiDiLevel NGCaretNavigator::BidiLevelAt(unsigned index) const { - DCHECK_LT(index, GetText().length()); - if (!IsBidiEnabled()) - return 0; - return GetData().FindItemForTextOffset(index).BidiLevel(); -} - -TextDirection NGCaretNavigator::TextDirectionAt(unsigned index) const { - UBiDiLevel level = BidiLevelAt(index); - return DirectionFromLevel(level); -} - -bool NGCaretNavigator::OffsetIsBidiBoundary(unsigned offset) const { - DCHECK_LE(offset, GetText().length()); - if (!IsBidiEnabled()) - return false; - if (!offset || offset == GetText().length()) - return false; - return BidiLevelAt(offset - 1) != BidiLevelAt(offset); -} - -NGCaretNavigator::Position -NGCaretNavigator::CaretPositionFromTextContentOffsetAndAffinity( - unsigned offset, - TextAffinity affinity) const { - DCHECK_LE(offset, GetText().length()); - // Callers sometimes pass in (0, upstream) or (length, downstream), which - // originate from legacy callers. Make sure they are fixed up. - // TODO(xiaochengh): Catch and eliminate such callers. - if (affinity == TextAffinity::kUpstream) { - if (offset) - return {offset - 1, PositionAnchorType::kAfter}; - return {0, PositionAnchorType::kBefore}; - } - - if (offset < GetText().length()) - return {offset, PositionAnchorType::kBefore}; - return {GetText().length() - 1, PositionAnchorType::kAfter}; -} - -// static -NGCaretNavigator::MoveDirection NGCaretNavigator::OppositeDirectionOf( - MoveDirection direction) { - if (direction == MoveDirection::kTowardsLeft) - return MoveDirection::kTowardsRight; - return MoveDirection::kTowardsLeft; -} - -// static -bool NGCaretNavigator::TowardsSameDirection(MoveDirection move_direction, - TextDirection text_direction) { - if (IsLtr(text_direction)) - return move_direction == MoveDirection::kTowardsRight; - return move_direction == MoveDirection::kTowardsLeft; -} - -NGCaretNavigator::Line NGCaretNavigator::ContainingLineOf( - unsigned index) const { - DCHECK_LT(index, GetText().length()); - // TODO(xiaochengh): Make it work for multi-col - DCHECK(context_.CurrentFragment()); - unsigned last_line_end = 0; - for (const auto child : context_.CurrentFragment()->Children()) { - if (!child->IsLineBox()) - continue; - const auto* line = To<NGPhysicalLineBoxFragment>(child.get()); - const auto* token = To<NGInlineBreakToken>(line->BreakToken()); - const unsigned line_end = - token->IsFinished() ? GetText().length() : token->TextOffset(); - if (line_end > index) - return {last_line_end, line_end, line->BaseDirection()}; - last_line_end = line_end; - } - NOTREACHED(); - return {}; -} - -bool NGCaretNavigator::IsValidCaretPosition(const Position& position) const { - unsigned index = position.index; - if (position.IsAfterCharacter() && IsLineBreak(index)) - return false; - if (IsCollapsedSpaceByLineWrap(index)) - return false; - if (IsIgnoredInCaretMovement(index)) - return false; - return true; -} - -bool NGCaretNavigator::IsCollapsibleWhitespace(unsigned index) const { - DCHECK_LT(index, GetText().length()); - if (GetText()[index] != kSpaceCharacter) - return false; - const NGInlineItem& item = GetData().FindItemForTextOffset(index); - return item.Style()->CollapseWhiteSpace(); -} - -bool NGCaretNavigator::IsLineBreak(unsigned index) const { - DCHECK_LT(index, GetText().length()); - return GetText()[index] == kNewlineCharacter; -} - -bool NGCaretNavigator::IsCollapsedSpaceByLineWrap(unsigned index) const { - DCHECK_LT(index, GetText().length()); - if (!IsCollapsibleWhitespace(index)) - return false; - return index + 1 == ContainingLineOf(index).end_offset; -} - -bool NGCaretNavigator::IsIgnoredInCaretMovement(unsigned index) const { - DCHECK_LT(index, GetText().length()); - const NGInlineItem& item = GetData().FindItemForTextOffset(index); - - // Caret navigation works on text, atomic inlines and non-ZWS controls only. - switch (item.Type()) { - case NGInlineItem::kText: - case NGInlineItem::kAtomicInline: - break; - case NGInlineItem::kControl: - if (GetText()[index] == kZeroWidthSpaceCharacter) - return true; - break; - default: - return true; - } - - // Ignore CSS generate contents. - // TODO(xiaochengh): This might be general enough to be merged into - // |NGInlineItem| as a member function. - DCHECK(item.GetLayoutObject()); - const LayoutObject* object = item.GetLayoutObject(); - if (const auto* text_fragment = ToLayoutTextFragmentOrNull(object)) { - // ::first-letter |LayoutTextFragment| returns null for |GetNode()|. Check - // |AssociatedTextNode()| to see if it's created by a text node. - if (!text_fragment->AssociatedTextNode()) - return true; - } else { - if (!object->NonPseudoNode()) - return true; - } - - // Ignore collapsed whitespaces that not visually at line end due to bidi. - // Caret movement should move over them as if they don't exist to match the - // existing behavior. - return IsCollapsedSpaceByLineWrap(index) && - index != VisualLastCharacterOf(ContainingLineOf(index)); -} - -bool NGCaretNavigator::IsEnterableChildContext(unsigned index) const { - DCHECK_LT(index, GetText().length()); - if (GetText()[index] != kObjectReplacementCharacter) - return false; - - const NGInlineItem& item = GetData().FindItemForTextOffset(index); - if (item.Type() != NGInlineItem::kAtomicInline) - return false; - DCHECK(item.GetLayoutObject()); - const LayoutObject* object = item.GetLayoutObject(); - if (!object->IsLayoutBlockFlow()) - return false; - if (!object->NonPseudoNode() || !object->GetNode()->IsElementNode()) - return false; - const Element* node = ToElement(object->GetNode()); - return !node->GetShadowRoot() || !node->GetShadowRoot()->IsUserAgent(); -} - -NGCaretNavigator::Position NGCaretNavigator::LeftEdgeOf(unsigned index) const { - return EdgeOfInternal(index, MoveDirection::kTowardsLeft); -} - -NGCaretNavigator::Position NGCaretNavigator::RightEdgeOf(unsigned index) const { - return EdgeOfInternal(index, MoveDirection::kTowardsRight); -} - -NGCaretNavigator::Position NGCaretNavigator::EdgeOfInternal( - unsigned index, - MoveDirection edge_direction) const { - DCHECK_LT(index, GetText().length()); - const TextDirection character_direction = TextDirectionAt(index); - return {index, TowardsSameDirection(edge_direction, character_direction) - ? PositionAnchorType::kAfter - : PositionAnchorType::kBefore}; -} - -NGCaretNavigator::VisualCharacterMovementResult -NGCaretNavigator::LeftCharacterOf(unsigned index) const { - return MoveCharacterInternal(index, MoveDirection::kTowardsLeft); -} - -NGCaretNavigator::VisualCharacterMovementResult -NGCaretNavigator::RightCharacterOf(unsigned index) const { - return MoveCharacterInternal(index, MoveDirection::kTowardsRight); -} - -Vector<int32_t, 32> NGCaretNavigator::CharacterIndicesInVisualOrder( - const Line& line) const { - DCHECK(IsBidiEnabled()); - - Vector<UBiDiLevel, 32> levels; - levels.ReserveCapacity(line.end_offset - line.start_offset); - for (unsigned i = line.start_offset; i < line.end_offset; ++i) - levels.push_back(BidiLevelAt(i)); - - Vector<int32_t, 32> indices(levels.size()); - NGBidiParagraph::IndicesInVisualOrder(levels, &indices); - - for (auto& index : indices) - index += line.start_offset; - return indices; -} - -unsigned NGCaretNavigator::VisualMostForwardCharacterOf( - const Line& line, - MoveDirection direction) const { - if (!IsBidiEnabled()) { - if (direction == MoveDirection::kTowardsLeft) - return line.start_offset; - return line.end_offset - 1; - } - - const auto indices_in_visual_order = CharacterIndicesInVisualOrder(line); - if (direction == MoveDirection::kTowardsLeft) - return indices_in_visual_order.front(); - return indices_in_visual_order.back(); -} - -unsigned NGCaretNavigator::VisualFirstCharacterOf(const Line& line) const { - return VisualMostForwardCharacterOf(line, IsLtr(line.base_direction) - ? MoveDirection::kTowardsLeft - : MoveDirection::kTowardsRight); -} - -unsigned NGCaretNavigator::VisualLastCharacterOf(const Line& line) const { - return VisualMostForwardCharacterOf(line, IsLtr(line.base_direction) - ? MoveDirection::kTowardsRight - : MoveDirection::kTowardsLeft); -} - -NGCaretNavigator::VisualCharacterMovementResult -NGCaretNavigator::MoveCharacterInternal(unsigned index, - MoveDirection move_direction) const { - const Line line = ContainingLineOf(index); - - if (index == VisualMostForwardCharacterOf(line, move_direction)) { - if (TowardsSameDirection(move_direction, line.base_direction)) { - if (line.end_offset == GetText().length()) - return {VisualMovementResultType::kAfterContext, base::nullopt}; - const Line next_line = ContainingLineOf(line.end_offset); - return {VisualMovementResultType::kWithinContext, - VisualFirstCharacterOf(next_line)}; - } - - if (!line.start_offset) - return {VisualMovementResultType::kBeforeContext, base::nullopt}; - const Line last_line = ContainingLineOf(line.start_offset - 1); - return {VisualMovementResultType::kWithinContext, - VisualLastCharacterOf(last_line)}; - } - - if (!IsBidiEnabled()) { - if (move_direction == MoveDirection::kTowardsLeft) - return {VisualMovementResultType::kWithinContext, index - 1}; - return {VisualMovementResultType::kWithinContext, index + 1}; - } - - Vector<int32_t, 32> indices_in_visual_order = - CharacterIndicesInVisualOrder(line); - const int32_t* visual_location = std::find( - indices_in_visual_order.begin(), indices_in_visual_order.end(), index); - DCHECK_NE(visual_location, indices_in_visual_order.end()); - if (move_direction == MoveDirection::kTowardsLeft) { - DCHECK_NE(visual_location, indices_in_visual_order.begin()); - return {VisualMovementResultType::kWithinContext, - *std::prev(visual_location)}; - } - DCHECK_NE(std::next(visual_location), indices_in_visual_order.end()); - return {VisualMovementResultType::kWithinContext, - *std::next(visual_location)}; -} - -NGCaretNavigator::VisualCaretMovementResult NGCaretNavigator::LeftPositionOf( - const Position& caret_position) const { - return MoveCaretInternal(caret_position, MoveDirection::kTowardsLeft); -} - -NGCaretNavigator::VisualCaretMovementResult NGCaretNavigator::RightPositionOf( - const Position& caret_position) const { - return MoveCaretInternal(caret_position, MoveDirection::kTowardsRight); -} - -NGCaretNavigator::UnvalidatedVisualCaretMovementResult -NGCaretNavigator::MoveCaretWithoutValidation( - const Position& caret_position, - MoveDirection move_direction) const { - const unsigned index = caret_position.index; - const MoveDirection opposite_direction = OppositeDirectionOf(move_direction); - if (caret_position == EdgeOfInternal(index, opposite_direction)) { - // TODO(xiaochengh): Consider grapheme cluster - return {VisualMovementResultType::kWithinContext, - EdgeOfInternal(index, move_direction), - !IsIgnoredInCaretMovement(index)}; - } - - VisualCharacterMovementResult forward_character = - MoveCharacterInternal(index, move_direction); - if (!forward_character.IsWithinContext()) - return {forward_character.type}; - - DCHECK(forward_character.index.has_value()); - const Position forward_caret = - EdgeOfInternal(*forward_character.index, opposite_direction); - return {VisualMovementResultType::kWithinContext, forward_caret}; -} - -NGCaretNavigator::VisualCaretMovementResult NGCaretNavigator::MoveCaretInternal( - const Position& caret_position, - MoveDirection move_direction) const { - bool has_passed_character = false; - base::Optional<Position> last_position; - for (Position runner = caret_position; - !has_passed_character || !IsValidCaretPosition(runner);) { - const UnvalidatedVisualCaretMovementResult next = - MoveCaretWithoutValidation(runner, move_direction); - if (next.type != VisualMovementResultType::kWithinContext) - return {next.type, base::nullopt}; - - if (next.has_passed_character) { - has_passed_character = true; - - const unsigned last_passed_character = next.position->index; - if (IsEnterableChildContext(last_passed_character)) - return {VisualMovementResultType::kEnteredChildContext, runner}; - } - - runner = *next.position; - last_position = runner; - - // TODO(xiaochengh): Handle the case where we reach a different line with a - // different base direction, which occurs with 'unicode-bidi: plain-text'. - } - DCHECK(last_position.has_value()); - return {VisualMovementResultType::kWithinContext, *last_position}; -} - -NGCaretNavigator::Position NGCaretNavigator::LeftmostPositionInFirstLine() - const { - Line first_line = ContainingLineOf(0); - unsigned leftmost_character = - VisualMostForwardCharacterOf(first_line, MoveDirection::kTowardsLeft); - // TODO(xiaochengh): Handle if the caret position is invalid. - return LeftEdgeOf(leftmost_character); -} - -NGCaretNavigator::Position NGCaretNavigator::RightmostPositionInFirstLine() - const { - Line first_line = ContainingLineOf(0); - unsigned rightmost_character = - VisualMostForwardCharacterOf(first_line, MoveDirection::kTowardsRight); - // TODO(xiaochengh): Handle if the caret position is invalid. - return RightEdgeOf(rightmost_character); -} - -NGCaretNavigator::Position NGCaretNavigator::LeftmostPositionInLastLine() - const { - Line last_line = ContainingLineOf(GetText().length() - 1); - unsigned leftmost_character = - VisualMostForwardCharacterOf(last_line, MoveDirection::kTowardsLeft); - // TODO(xiaochengh): Handle if the caret position is invalid. - return LeftEdgeOf(leftmost_character); -} - -NGCaretNavigator::Position NGCaretNavigator::RightmostPositionInLastLine() - const { - Line last_line = ContainingLineOf(GetText().length() - 1); - unsigned rightmost_character = - VisualMostForwardCharacterOf(last_line, MoveDirection::kTowardsRight); - // TODO(xiaochengh): Handle if the caret position is invalid. - return RightEdgeOf(rightmost_character); -} - -} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_navigator.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_navigator.h deleted file mode 100644 index 6c60d7f9f90..00000000000 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_navigator.h +++ /dev/null @@ -1,215 +0,0 @@ -// 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_CARET_NAVIGATOR_H_ -#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_CARET_NAVIGATOR_H_ - -#include "base/optional.h" -#include "third_party/blink/renderer/core/core_export.h" -#include "third_party/blink/renderer/core/dom/document_lifecycle.h" -#include "third_party/blink/renderer/core/editing/text_affinity.h" -#include "third_party/blink/renderer/platform/text/text_direction.h" -#include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" -#include "third_party/blink/renderer/platform/wtf/vector.h" - -#include <unicode/ubidi.h> - -namespace blink { - -class LayoutBlockFlow; -struct NGInlineNodeData; - -// Hosts the |text_content| of an inline formatting context and provides -// bidi-related utilities, including checking bidi levels, computing visual -// left/right characters and visual left/right caret movements. -// Design doc: http://bit.ly/2QVAwGq -class CORE_EXPORT NGCaretNavigator { - STACK_ALLOCATED(); - - public: - explicit NGCaretNavigator(const LayoutBlockFlow&); - ~NGCaretNavigator(); - - const String& GetText() const; - bool IsBidiEnabled() const; - - // Abstraction of a caret position in |text_|. - enum class PositionAnchorType { kBefore, kAfter }; - struct Position { - // |index| is character index the |text_| string. - unsigned index; - PositionAnchorType type; - - bool IsBeforeCharacter() const { - return type == PositionAnchorType::kBefore; - } - - bool IsAfterCharacter() const { return type == PositionAnchorType::kAfter; } - - bool operator==(const Position& other) const { - return index == other.index && type == other.type; - } - }; - - // Returns the bidi level or resolved direction of the character at the given - // logical |index|. - UBiDiLevel BidiLevelAt(unsigned index) const; - TextDirection TextDirectionAt(unsigned index) const; - - // Returns true if the characters at indexes |offset - 1| and |offset| both - // exist and are at different bidi levels. - bool OffsetIsBidiBoundary(unsigned offset) const; - - // Converts an (offset, affinity) pair into a |Position| type of this class. - // Intiontionally long name to indicate the hackiness for handling legacy - // callers. - Position CaretPositionFromTextContentOffsetAndAffinity( - unsigned offset, - TextAffinity affinity) const; - - // Returns the visual left/right edge caret position of the character at the - // given logical |index|. - Position LeftEdgeOf(unsigned index) const; - Position RightEdgeOf(unsigned index) const; - - // Left/right visual movements - // TODO(xiaochengh): Handle the following - // - Grapheme clusters - - enum class VisualMovementResultType { - kWithinContext, - kBeforeContext, - kAfterContext, - kEnteredChildContext - }; - - // Given the character at the logical |index|, returns the logical index of - // the character at its left/right side. - struct VisualCharacterMovementResult { - bool IsWithinContext() const { - return type == VisualMovementResultType::kWithinContext; - } - bool IsBeforeContext() const { - return type == VisualMovementResultType::kBeforeContext; - } - bool IsAfterContext() const { - return type == VisualMovementResultType::kAfterContext; - } - - VisualMovementResultType type; - base::Optional<unsigned> index; - }; - VisualCharacterMovementResult LeftCharacterOf(unsigned index) const; - VisualCharacterMovementResult RightCharacterOf(unsigned index) const; - - // Given a caret position, moves it left/right by one grapheme cluster and - // returns the result. - // Note: If we end up entering an inline block, the result |Position| is - // either before or after the inline block, depending on from which side the - // inline block is entered. For example: - // RightPositionOf(abc|<inline-block>def</inline-block>ghi) - // -> {inline-block, PositionAnchorType::kBefore} - // LeftPositionOf(abc<inline-block>def</inline-block>|ghi) - // -> {inline-block, PositionAnchorType::kAfter} - struct VisualCaretMovementResult { - bool IsWithinContext() const { - return type == VisualMovementResultType::kWithinContext; - } - bool IsBeforeContext() const { - return type == VisualMovementResultType::kBeforeContext; - } - bool IsAfterContext() const { - return type == VisualMovementResultType::kAfterContext; - } - bool HasEnteredChildContext() const { - return type == VisualMovementResultType::kEnteredChildContext; - } - - VisualMovementResultType type; - base::Optional<Position> position; - }; - VisualCaretMovementResult LeftPositionOf(const Position&) const; - VisualCaretMovementResult RightPositionOf(const Position&) const; - - // TODO(xiaochengh): Specify and implement the behavior in edge cases, e.g., - // when the leftmost character of the first line is CSS-generated. - Position LeftmostPositionInFirstLine() const; - Position RightmostPositionInFirstLine() const; - Position LeftmostPositionInLastLine() const; - Position RightmostPositionInLastLine() const; - - private: - // A caret position is invalid if it is: - // - kAfter to a line break character. - // - Anchored to a collapsible space that's removed by line wrap. - // - Anchored to a character that's ignored in caret movement. - bool IsValidCaretPosition(const Position&) const; - bool IsLineBreak(unsigned index) const; - bool IsCollapsibleWhitespace(unsigned index) const; - bool IsCollapsedSpaceByLineWrap(unsigned index) const; - bool IsIgnoredInCaretMovement(unsigned index) const; - - // Returns true if the character at |index| represents a child block - // formatting context that can be entered by caret navigation. Such contexts - // must be atomic inlines (inline block, inline table, ...) and must not host - // user agent shadow tree (which excludes, e.g., <input> and image alt text). - bool IsEnterableChildContext(unsigned index) const; - - enum class MoveDirection { kTowardsLeft, kTowardsRight }; - static MoveDirection OppositeDirectionOf(MoveDirection); - static bool TowardsSameDirection(MoveDirection, TextDirection); - - // ------ Line-related functions ------ - - // A line contains a consecutive substring of |GetText()|. The lines should - // not overlap, and should together cover the entire |GetText()|. - struct Line { - unsigned start_offset; - unsigned end_offset; - TextDirection base_direction; - }; - Line ContainingLineOf(unsigned index) const; - Vector<int32_t, 32> CharacterIndicesInVisualOrder(const Line&) const; - unsigned VisualMostForwardCharacterOf(const Line&, - MoveDirection direction) const; - unsigned VisualLastCharacterOf(const Line&) const; - unsigned VisualFirstCharacterOf(const Line&) const; - - // ------ Implementation of public visual movement functions ------ - - Position EdgeOfInternal(unsigned index, MoveDirection) const; - VisualCharacterMovementResult MoveCharacterInternal(unsigned index, - MoveDirection) const; - VisualCaretMovementResult MoveCaretInternal(const Position&, - MoveDirection) const; - - // Performs a "minimal" caret movement to the left/right without validating - // the result. The result might be invalid due to, e.g., anchored to an - // unallowed character, being visually the same as the input, etc. It's a - // subroutine of |MoveCaretInternal|, who keeps calling it until both of the - // folliwng are satisfied: - // - We've reached a valid caret position. - // - During the process, the caret has moved passing a character on which - // |IsIgnoredInCaretMovement| is false (indicated by |has_passed_character|). - struct UnvalidatedVisualCaretMovementResult { - VisualMovementResultType type; - base::Optional<Position> position; - bool has_passed_character = false; - }; - UnvalidatedVisualCaretMovementResult MoveCaretWithoutValidation( - const Position&, - MoveDirection) const; - - const NGInlineNodeData& GetData() const; - - const LayoutBlockFlow& context_; - DocumentLifecycle::DisallowTransitionScope disallow_transition_; -}; - -CORE_EXPORT std::ostream& operator<<(std::ostream&, - const NGCaretNavigator::Position&); - -} // namespace blink - -#endif // THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_CARET_NAVIGATOR_H_ diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_navigator_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_navigator_test.cc deleted file mode 100644 index 70c99450dd9..00000000000 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_navigator_test.cc +++ /dev/null @@ -1,461 +0,0 @@ -// 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_caret_navigator.h" - -#include "third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.h" -#include "third_party/blink/renderer/core/layout/ng/ng_layout_test.h" -#include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h" - -namespace blink { - -class NGCaretNavigatorTest : public RenderingTest, - private ScopedBidiCaretAffinityForTest { - public: - NGCaretNavigatorTest() : ScopedBidiCaretAffinityForTest(true) {} - - void SetupHtml(const char* id, String html) { - SetBodyInnerHTML(html); - - block_flow_ = To<LayoutBlockFlow>(GetLayoutObjectByElementId(id)); - DCHECK(block_flow_); - DCHECK(block_flow_->IsLayoutNGMixin()); - DCHECK(block_flow_->ChildrenInline()); - } - - UBiDiLevel BidiLevelAt(unsigned index) const { - return NGCaretNavigator(*block_flow_).BidiLevelAt(index); - } - - NGCaretNavigator::VisualCharacterMovementResult LeftCharacterOf( - unsigned index) const { - return NGCaretNavigator(*block_flow_).LeftCharacterOf(index); - } - - NGCaretNavigator::VisualCharacterMovementResult RightCharacterOf( - unsigned index) const { - return NGCaretNavigator(*block_flow_).RightCharacterOf(index); - } - - NGCaretNavigator::Position CaretBefore(unsigned index) const { - return {index, NGCaretNavigator::PositionAnchorType::kBefore}; - } - - NGCaretNavigator::Position CaretAfter(unsigned index) const { - return {index, NGCaretNavigator::PositionAnchorType::kAfter}; - } - - NGCaretNavigator::VisualCaretMovementResult LeftPositionOf( - const NGCaretNavigator::Position& position) const { - return NGCaretNavigator(*block_flow_).LeftPositionOf(position); - } - - NGCaretNavigator::VisualCaretMovementResult RightPositionOf( - const NGCaretNavigator::Position& position) const { - return NGCaretNavigator(*block_flow_).RightPositionOf(position); - } - - protected: - const LayoutBlockFlow* block_flow_; -}; - -TEST_F(NGCaretNavigatorTest, BidiLevelAtBasic) { - SetupHtml("container", - "<div id=container>abcאבג123</div>"); - - EXPECT_EQ(0u, BidiLevelAt(0)); - EXPECT_EQ(0u, BidiLevelAt(1)); - EXPECT_EQ(0u, BidiLevelAt(2)); - EXPECT_EQ(1u, BidiLevelAt(3)); - EXPECT_EQ(1u, BidiLevelAt(4)); - EXPECT_EQ(1u, BidiLevelAt(5)); - EXPECT_EQ(2u, BidiLevelAt(6)); - EXPECT_EQ(2u, BidiLevelAt(7)); - EXPECT_EQ(2u, BidiLevelAt(8)); -} - -TEST_F(NGCaretNavigatorTest, LeftCharacterOfBasic) { - SetupHtml("container", - "<div id=container>abcאבג123</div>"); - - EXPECT_TRUE(LeftCharacterOf(0).IsBeforeContext()); - - EXPECT_TRUE(LeftCharacterOf(1).IsWithinContext()); - EXPECT_EQ(0u, *LeftCharacterOf(1).index); - - EXPECT_TRUE(LeftCharacterOf(2).IsWithinContext()); - EXPECT_EQ(1u, *LeftCharacterOf(2).index); - - EXPECT_TRUE(LeftCharacterOf(3).IsWithinContext()); - EXPECT_EQ(4u, *LeftCharacterOf(3).index); - - EXPECT_TRUE(LeftCharacterOf(4).IsWithinContext()); - EXPECT_EQ(5u, *LeftCharacterOf(4).index); - - EXPECT_TRUE(LeftCharacterOf(5).IsWithinContext()); - EXPECT_EQ(8u, *LeftCharacterOf(5).index); - - EXPECT_TRUE(LeftCharacterOf(6).IsWithinContext()); - EXPECT_EQ(2u, *LeftCharacterOf(6).index); - - EXPECT_TRUE(LeftCharacterOf(7).IsWithinContext()); - EXPECT_EQ(6u, *LeftCharacterOf(7).index); - - EXPECT_TRUE(LeftCharacterOf(8).IsWithinContext()); - EXPECT_EQ(7u, *LeftCharacterOf(8).index); -} - -TEST_F(NGCaretNavigatorTest, RightCharacterOfBasic) { - SetupHtml("container", - "<div id=container>abcאבג123</div>"); - - EXPECT_TRUE(RightCharacterOf(0).IsWithinContext()); - EXPECT_EQ(1u, *RightCharacterOf(0).index); - - EXPECT_TRUE(RightCharacterOf(1).IsWithinContext()); - EXPECT_EQ(2u, *RightCharacterOf(1).index); - - EXPECT_TRUE(RightCharacterOf(2).IsWithinContext()); - EXPECT_EQ(6u, *RightCharacterOf(2).index); - - EXPECT_TRUE(RightCharacterOf(3).IsAfterContext()); - - EXPECT_TRUE(RightCharacterOf(4).IsWithinContext()); - EXPECT_EQ(3u, *RightCharacterOf(4).index); - - EXPECT_TRUE(RightCharacterOf(5).IsWithinContext()); - EXPECT_EQ(4u, *RightCharacterOf(5).index); - - EXPECT_TRUE(RightCharacterOf(6).IsWithinContext()); - EXPECT_EQ(7u, *RightCharacterOf(6).index); - - EXPECT_TRUE(RightCharacterOf(7).IsWithinContext()); - EXPECT_EQ(8u, *RightCharacterOf(7).index); - - EXPECT_TRUE(RightCharacterOf(8).IsWithinContext()); - EXPECT_EQ(5u, *RightCharacterOf(8).index); -} - -TEST_F(NGCaretNavigatorTest, LeftPositionOfBasic) { - SetupHtml("container", - "<div id=container>abcאבג123</div>"); - - EXPECT_TRUE(LeftPositionOf(CaretBefore(0)).IsBeforeContext()); - - EXPECT_TRUE(LeftPositionOf(CaretAfter(0)).IsWithinContext()); - EXPECT_EQ(CaretBefore(0), *LeftPositionOf(CaretAfter(0)).position); - - EXPECT_TRUE(LeftPositionOf(CaretBefore(1)).IsWithinContext()); - EXPECT_EQ(CaretBefore(0), *LeftPositionOf(CaretBefore(1)).position); - - EXPECT_TRUE(LeftPositionOf(CaretAfter(1)).IsWithinContext()); - EXPECT_EQ(CaretBefore(1), *LeftPositionOf(CaretAfter(1)).position); - - EXPECT_TRUE(LeftPositionOf(CaretBefore(2)).IsWithinContext()); - EXPECT_EQ(CaretBefore(1), *LeftPositionOf(CaretBefore(2)).position); - - EXPECT_TRUE(LeftPositionOf(CaretAfter(2)).IsWithinContext()); - EXPECT_EQ(CaretBefore(2), *LeftPositionOf(CaretAfter(2)).position); - - EXPECT_TRUE(LeftPositionOf(CaretBefore(3)).IsWithinContext()); - EXPECT_EQ(CaretAfter(3), *LeftPositionOf(CaretBefore(3)).position); - - EXPECT_TRUE(LeftPositionOf(CaretAfter(3)).IsWithinContext()); - EXPECT_EQ(CaretAfter(4), *LeftPositionOf(CaretAfter(3)).position); - - EXPECT_TRUE(LeftPositionOf(CaretBefore(4)).IsWithinContext()); - EXPECT_EQ(CaretAfter(4), *LeftPositionOf(CaretBefore(4)).position); - - EXPECT_TRUE(LeftPositionOf(CaretAfter(4)).IsWithinContext()); - EXPECT_EQ(CaretAfter(5), *LeftPositionOf(CaretAfter(4)).position); - - EXPECT_TRUE(LeftPositionOf(CaretBefore(5)).IsWithinContext()); - EXPECT_EQ(CaretAfter(5), *LeftPositionOf(CaretBefore(5)).position); - - EXPECT_TRUE(LeftPositionOf(CaretAfter(5)).IsWithinContext()); - EXPECT_EQ(CaretBefore(8), *LeftPositionOf(CaretAfter(5)).position); - - EXPECT_TRUE(LeftPositionOf(CaretBefore(6)).IsWithinContext()); - EXPECT_EQ(CaretBefore(2), *LeftPositionOf(CaretBefore(6)).position); - - EXPECT_TRUE(LeftPositionOf(CaretAfter(6)).IsWithinContext()); - EXPECT_EQ(CaretBefore(6), *LeftPositionOf(CaretAfter(6)).position); - - EXPECT_TRUE(LeftPositionOf(CaretBefore(7)).IsWithinContext()); - EXPECT_EQ(CaretBefore(6), *LeftPositionOf(CaretBefore(7)).position); - - EXPECT_TRUE(LeftPositionOf(CaretAfter(7)).IsWithinContext()); - EXPECT_EQ(CaretBefore(7), *LeftPositionOf(CaretAfter(7)).position); - - EXPECT_TRUE(LeftPositionOf(CaretBefore(8)).IsWithinContext()); - EXPECT_EQ(CaretBefore(7), *LeftPositionOf(CaretBefore(8)).position); - - EXPECT_TRUE(LeftPositionOf(CaretAfter(8)).IsWithinContext()); - EXPECT_EQ(CaretBefore(8), *LeftPositionOf(CaretAfter(8)).position); -} - -TEST_F(NGCaretNavigatorTest, RightPositionOfBasic) { - SetupHtml("container", - "<div id=container>abcאבג123</div>"); - - EXPECT_TRUE(RightPositionOf(CaretBefore(0)).IsWithinContext()); - EXPECT_EQ(CaretAfter(0), *RightPositionOf(CaretBefore(0)).position); - - EXPECT_TRUE(RightPositionOf(CaretAfter(0)).IsWithinContext()); - EXPECT_EQ(CaretAfter(1), *RightPositionOf(CaretAfter(0)).position); - - EXPECT_TRUE(RightPositionOf(CaretBefore(1)).IsWithinContext()); - EXPECT_EQ(CaretAfter(1), *RightPositionOf(CaretBefore(1)).position); - - EXPECT_TRUE(RightPositionOf(CaretAfter(1)).IsWithinContext()); - EXPECT_EQ(CaretAfter(2), *RightPositionOf(CaretAfter(1)).position); - - EXPECT_TRUE(RightPositionOf(CaretBefore(2)).IsWithinContext()); - EXPECT_EQ(CaretAfter(2), *RightPositionOf(CaretBefore(2)).position); - - EXPECT_TRUE(RightPositionOf(CaretAfter(2)).IsWithinContext()); - EXPECT_EQ(CaretAfter(6), *RightPositionOf(CaretAfter(2)).position); - - EXPECT_TRUE(RightPositionOf(CaretBefore(3)).IsAfterContext()); - - EXPECT_TRUE(RightPositionOf(CaretAfter(3)).IsWithinContext()); - EXPECT_EQ(CaretBefore(3), *RightPositionOf(CaretAfter(3)).position); - - EXPECT_TRUE(RightPositionOf(CaretBefore(4)).IsWithinContext()); - EXPECT_EQ(CaretBefore(3), *RightPositionOf(CaretBefore(4)).position); - - EXPECT_TRUE(RightPositionOf(CaretAfter(4)).IsWithinContext()); - EXPECT_EQ(CaretBefore(4), *RightPositionOf(CaretAfter(4)).position); - - EXPECT_TRUE(RightPositionOf(CaretBefore(5)).IsWithinContext()); - EXPECT_EQ(CaretBefore(4), *RightPositionOf(CaretBefore(5)).position); - - EXPECT_TRUE(RightPositionOf(CaretAfter(5)).IsWithinContext()); - EXPECT_EQ(CaretBefore(5), *RightPositionOf(CaretAfter(5)).position); - - EXPECT_TRUE(RightPositionOf(CaretBefore(6)).IsWithinContext()); - EXPECT_EQ(CaretAfter(6), *RightPositionOf(CaretBefore(6)).position); - - EXPECT_TRUE(RightPositionOf(CaretAfter(6)).IsWithinContext()); - EXPECT_EQ(CaretAfter(7), *RightPositionOf(CaretAfter(6)).position); - - EXPECT_TRUE(RightPositionOf(CaretBefore(7)).IsWithinContext()); - EXPECT_EQ(CaretAfter(7), *RightPositionOf(CaretBefore(7)).position); - - EXPECT_TRUE(RightPositionOf(CaretAfter(7)).IsWithinContext()); - EXPECT_EQ(CaretAfter(8), *RightPositionOf(CaretAfter(7)).position); - - EXPECT_TRUE(RightPositionOf(CaretBefore(8)).IsWithinContext()); - EXPECT_EQ(CaretAfter(8), *RightPositionOf(CaretBefore(8)).position); - - EXPECT_TRUE(RightPositionOf(CaretAfter(8)).IsWithinContext()); - EXPECT_EQ(CaretBefore(5), *RightPositionOf(CaretAfter(8)).position); -} - -// Tests below check caret movement crossing line boundaries - -TEST_F(NGCaretNavigatorTest, HardLineBreak) { - SetupHtml("container", "<div id=container>abc<br>def</div>"); - - EXPECT_TRUE(LeftPositionOf(CaretBefore(0)).IsBeforeContext()); - - EXPECT_TRUE(RightPositionOf(CaretAfter(2)).IsWithinContext()); - EXPECT_EQ(CaretBefore(4), *RightPositionOf(CaretAfter(2)).position); - - EXPECT_TRUE(RightPositionOf(CaretBefore(3)).IsWithinContext()); - EXPECT_EQ(CaretBefore(4), *RightPositionOf(CaretBefore(3)).position); - - EXPECT_TRUE(LeftPositionOf(CaretBefore(4)).IsWithinContext()); - EXPECT_EQ(CaretBefore(3), *LeftPositionOf(CaretBefore(4)).position); - - EXPECT_TRUE(RightPositionOf(CaretAfter(6)).IsAfterContext()); -} - -TEST_F(NGCaretNavigatorTest, SoftLineWrapAtSpace) { - SetupHtml("container", "<div id=container style=\"width:0\">abc def</div>"); - - EXPECT_TRUE(LeftPositionOf(CaretBefore(0)).IsBeforeContext()); - - EXPECT_TRUE(RightPositionOf(CaretAfter(2)).IsWithinContext()); - EXPECT_EQ(CaretBefore(4), *RightPositionOf(CaretAfter(2)).position); - - EXPECT_TRUE(RightPositionOf(CaretBefore(3)).IsWithinContext()); - EXPECT_EQ(CaretBefore(4), *RightPositionOf(CaretBefore(3)).position); - - EXPECT_TRUE(LeftPositionOf(CaretBefore(4)).IsWithinContext()); - EXPECT_EQ(CaretAfter(2), *LeftPositionOf(CaretBefore(4)).position); - - EXPECT_TRUE(RightPositionOf(CaretAfter(6)).IsAfterContext()); -} - -TEST_F(NGCaretNavigatorTest, BidiAndSoftLineWrapAtSpaceLtr) { - LoadAhem(); - SetupHtml("container", - "<div id=container style='font: 10px/10px Ahem; width: 100px'>" - "before אבגד " - "הוזחטי" - "ךכלםמן" - "נסעףפץ" - "</div>"); - - // Moving left from "|before DCBA" should be before context - EXPECT_TRUE(LeftPositionOf(CaretBefore(0)).IsBeforeContext()); - - // Moving right from "before |DCBA" should yield "before D|CBA" - EXPECT_TRUE(RightPositionOf(CaretAfter(10)).IsWithinContext()); - EXPECT_EQ(CaretBefore(10), *RightPositionOf(CaretAfter(10)).position); - EXPECT_TRUE(RightPositionOf(CaretAfter(6)).IsWithinContext()); - EXPECT_EQ(CaretBefore(10), *RightPositionOf(CaretAfter(6)).position); - - // Moving left from "before |DCBA" should yield "before| DCBA" - EXPECT_TRUE(LeftPositionOf(CaretAfter(10)).IsWithinContext()); - EXPECT_EQ(CaretBefore(6), *LeftPositionOf(CaretAfter(10)).position); - EXPECT_TRUE(LeftPositionOf(CaretAfter(6)).IsWithinContext()); - EXPECT_EQ(CaretBefore(6), *LeftPositionOf(CaretAfter(6)).position); - - // Moving right from "before DCBA|" should yield "V|UTSRQPONMLKJIHGFE" - EXPECT_TRUE(RightPositionOf(CaretBefore(7)).IsWithinContext()); - EXPECT_EQ(CaretBefore(29), *RightPositionOf(CaretBefore(7)).position); - - // Moving left from "|VUTSRQPONMLKJIHGFE" should yield "before DCB|A" - EXPECT_TRUE(LeftPositionOf(CaretAfter(29)).IsWithinContext()); - EXPECT_EQ(CaretAfter(7), *LeftPositionOf(CaretAfter(29)).position); - - // Moving right from "VUTSRQPONMLKJIHGFE|" should be after context - EXPECT_TRUE(RightPositionOf(CaretBefore(12)).IsAfterContext()); -} - -TEST_F(NGCaretNavigatorTest, BidiAndSoftLineWrapAtSpaceRtl) { - LoadAhem(); - SetupHtml( - "container", - "<div dir=rtl id=container style='font: 10px/10px Ahem; width: 120px'>" - "אבגד after encyclopedia" - "</div>"); - - // Moving right from "after DCBA|" should be before context - EXPECT_TRUE(RightPositionOf(CaretBefore(0)).IsBeforeContext()); - - // Moving left from "after| DCBA" should yield "afte|r DCBA" - EXPECT_TRUE(LeftPositionOf(CaretAfter(4)).IsWithinContext()); - EXPECT_EQ(CaretBefore(9), *LeftPositionOf(CaretAfter(4)).position); - EXPECT_TRUE(LeftPositionOf(CaretAfter(9)).IsWithinContext()); - EXPECT_EQ(CaretBefore(9), *LeftPositionOf(CaretAfter(9)).position); - - // Moving right from "after| DCBA" should yield "after |DCBA" - EXPECT_TRUE(RightPositionOf(CaretAfter(4)).IsWithinContext()); - EXPECT_EQ(CaretBefore(4), *RightPositionOf(CaretAfter(4)).position); - EXPECT_TRUE(RightPositionOf(CaretAfter(9)).IsWithinContext()); - EXPECT_EQ(CaretBefore(4), *RightPositionOf(CaretAfter(9)).position); - - // Moving left from "|after DCBA" should yield "encyclopedi|a" - EXPECT_TRUE(LeftPositionOf(CaretBefore(5)).IsWithinContext()); - EXPECT_EQ(CaretBefore(22), *LeftPositionOf(CaretBefore(5)).position); - - // Moving right from "encyclopedia|" should yield "a|fter DCBA" - EXPECT_TRUE(RightPositionOf(CaretAfter(22)).IsWithinContext()); - EXPECT_EQ(CaretAfter(5), *RightPositionOf(CaretAfter(22)).position); - - // Moving left from "|encyclopedia" should be after context - EXPECT_TRUE(LeftPositionOf(CaretBefore(11)).IsAfterContext()); -} - -TEST_F(NGCaretNavigatorTest, SoftLineWrapAtHyphen) { - SetupHtml("container", "<div id=container style=\"width:0\">abc-def</div>"); - - EXPECT_TRUE(LeftPositionOf(CaretBefore(0)).IsBeforeContext()); - - // 3 -> 4 - EXPECT_TRUE(RightPositionOf(CaretAfter(2)).IsWithinContext()); - EXPECT_EQ(CaretAfter(3), *RightPositionOf(CaretAfter(2)).position); - EXPECT_TRUE(RightPositionOf(CaretBefore(3)).IsWithinContext()); - EXPECT_EQ(CaretAfter(3), *RightPositionOf(CaretBefore(3)).position); - - // 4 -> 5 - EXPECT_TRUE(RightPositionOf(CaretAfter(3)).IsWithinContext()); - EXPECT_EQ(CaretAfter(4), *RightPositionOf(CaretAfter(3)).position); - EXPECT_TRUE(RightPositionOf(CaretBefore(4)).IsWithinContext()); - EXPECT_EQ(CaretAfter(4), *RightPositionOf(CaretBefore(4)).position); - - // 5 -> 4 - EXPECT_TRUE(LeftPositionOf(CaretBefore(5)).IsWithinContext()); - EXPECT_EQ(CaretBefore(4), *LeftPositionOf(CaretBefore(5)).position); - EXPECT_TRUE(LeftPositionOf(CaretAfter(4)).IsWithinContext()); - EXPECT_EQ(CaretBefore(4), *LeftPositionOf(CaretAfter(4)).position); - - // 4 -> 3 - EXPECT_TRUE(LeftPositionOf(CaretBefore(4)).IsWithinContext()); - EXPECT_EQ(CaretBefore(3), *LeftPositionOf(CaretBefore(4)).position); - EXPECT_TRUE(LeftPositionOf(CaretAfter(3)).IsWithinContext()); - EXPECT_EQ(CaretBefore(3), *LeftPositionOf(CaretAfter(3)).position); - - EXPECT_TRUE(RightPositionOf(CaretAfter(6)).IsAfterContext()); -} - -TEST_F(NGCaretNavigatorTest, MoveOverPseudoElementInBidi) { - SetupHtml("container", - "<style>.bidi::before,.bidi::after{content:'a\\05D0 b'}</style>" - "<div id=container>אב גד " - "<span class=bidi>הו</span>" - " זח טי</div>"); - - // Text: "AB CD aAbEFaAb GH IJ" - // Rendered as: "DC BA aAbFEaAb JI HG" - - // Moving right from "BA |" should arrive at "F|E" - EXPECT_TRUE(RightPositionOf(CaretAfter(5)).IsWithinContext()); - EXPECT_EQ(CaretBefore(10), *RightPositionOf(CaretAfter(5)).position); - - // Moving left from "|FE" should arrive at "BA| " - EXPECT_TRUE(LeftPositionOf(CaretAfter(10)).IsWithinContext()); - EXPECT_EQ(CaretBefore(5), *LeftPositionOf(CaretAfter(10)).position); - - // Moving right from "FE|" should arrive at " |JI" - EXPECT_TRUE(RightPositionOf(CaretBefore(9)).IsWithinContext()); - EXPECT_EQ(CaretAfter(14), *RightPositionOf(CaretBefore(9)).position); - - // Moving left from "| JI" should arrive at "F|E" - EXPECT_TRUE(LeftPositionOf(CaretBefore(14)).IsWithinContext()); - EXPECT_EQ(CaretAfter(9), *LeftPositionOf(CaretBefore(14)).position); -} - -TEST_F(NGCaretNavigatorTest, EnterableInlineBlock) { - SetupHtml("container", - "<div id=container>foo" - "<span style='display:inline-block'>bar</span>" - "baz</div>"); - - // Moving right from "foo|" should enter the span from front. - EXPECT_TRUE(RightPositionOf(CaretAfter(2)).HasEnteredChildContext()); - EXPECT_EQ(CaretBefore(3), *RightPositionOf(CaretAfter(2)).position); - EXPECT_TRUE(RightPositionOf(CaretBefore(3)).HasEnteredChildContext()); - EXPECT_EQ(CaretBefore(3), *RightPositionOf(CaretBefore(3)).position); - - // Moving left from "|baz" should enter the span from behind. - EXPECT_TRUE(LeftPositionOf(CaretBefore(4)).HasEnteredChildContext()); - EXPECT_EQ(CaretAfter(3), *LeftPositionOf(CaretBefore(4)).position); - EXPECT_TRUE(LeftPositionOf(CaretAfter(3)).HasEnteredChildContext()); - EXPECT_EQ(CaretAfter(3), *LeftPositionOf(CaretAfter(3)).position); -} - -TEST_F(NGCaretNavigatorTest, UnenterableInlineBlock) { - SetupHtml("container", - "<div id=container>foo" - "<input value=bar>" - "baz</div>"); - - // Moving right from "foo|" should reach "<input>|". - EXPECT_TRUE(RightPositionOf(CaretAfter(2)).IsWithinContext()); - EXPECT_EQ(CaretAfter(3), *RightPositionOf(CaretAfter(2)).position); - EXPECT_TRUE(RightPositionOf(CaretBefore(3)).IsWithinContext()); - EXPECT_EQ(CaretAfter(3), *RightPositionOf(CaretBefore(3)).position); - - // Moving left from "|baz" should reach "|<input>". - EXPECT_TRUE(LeftPositionOf(CaretBefore(4)).IsWithinContext()); - EXPECT_EQ(CaretBefore(3), *LeftPositionOf(CaretBefore(4)).position); - EXPECT_TRUE(LeftPositionOf(CaretAfter(3)).IsWithinContext()); - EXPECT_EQ(CaretBefore(3), *LeftPositionOf(CaretAfter(3)).position); -} - -} // namespace blink 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 00c6d5ee3ed..01ea3fc7cdc 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 @@ -19,16 +19,6 @@ namespace blink { namespace { -void AssertValidPositionForCaretPositionComputation( - const PositionWithAffinity& position) { -#if DCHECK_IS_ON() - DCHECK(NGOffsetMapping::AcceptsPosition(position.GetPosition())); - const LayoutObject* layout_object = position.AnchorNode()->GetLayoutObject(); - DCHECK(layout_object); - DCHECK(layout_object->IsText() || layout_object->IsAtomicInlineLevel()); -#endif -} - // The calculation takes the following input: // - An inline formatting context as a |LayoutBlockFlow| // - An offset in the |text_content_| string of the above context @@ -97,7 +87,7 @@ CaretPositionResolution TryResolveCaretPositionInTextFragment( TextAffinity affinity) { const auto& fragment = To<NGPhysicalTextFragment>(paint_fragment.PhysicalFragment()); - if (fragment.IsAnonymousText()) + if (fragment.IsGeneratedText()) return CaretPositionResolution(); const NGOffsetMapping& mapping = @@ -306,7 +296,6 @@ NGCaretPosition ComputeNGCaretPosition(const LayoutBlockFlow& context, } NGCaretPosition ComputeNGCaretPosition(const PositionWithAffinity& position) { - AssertValidPositionForCaretPositionComputation(position); LayoutBlockFlow* context = NGInlineFormattingContextOf(position.GetPosition()); if (!context) diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_rect.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_rect.cc index 97c719d8e96..4149569a048 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_rect.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_rect.cc @@ -6,8 +6,8 @@ #include "third_party/blink/renderer/core/editing/local_caret_rect.h" #include "third_party/blink/renderer/core/frame/local_frame_view.h" +#include "third_party/blink/renderer/core/layout/geometry/physical_rect.h" #include "third_party/blink/renderer/core/layout/layout_block_flow.h" -#include "third_party/blink/renderer/core/layout/ng/geometry/ng_physical_offset_rect.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.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" @@ -16,13 +16,12 @@ namespace blink { namespace { -NGPhysicalOffsetRect ComputeLocalCaretRectByBoxSide( - const NGPaintFragment& fragment, - NGCaretPositionType position_type) { +PhysicalRect ComputeLocalCaretRectByBoxSide(const NGPaintFragment& fragment, + NGCaretPositionType position_type) { const bool is_horizontal = fragment.Style().IsHorizontalWritingMode(); DCHECK(fragment.ContainerLineBox()); const NGPaintFragment& line_box = *fragment.ContainerLineBox(); - const NGPhysicalOffset offset_to_line_box = + const PhysicalOffset offset_to_line_box = fragment.InlineOffsetToContainerBox() - line_box.InlineOffsetToContainerBox(); LayoutUnit caret_height = @@ -48,12 +47,12 @@ NGPhysicalOffsetRect ComputeLocalCaretRectByBoxSide( std::swap(caret_width, caret_height); } - const NGPhysicalOffset caret_location(caret_left, caret_top); - const NGPhysicalSize caret_size(caret_width, caret_height); - return NGPhysicalOffsetRect(caret_location, caret_size); + const PhysicalOffset caret_location(caret_left, caret_top); + const PhysicalSize caret_size(caret_width, caret_height); + return PhysicalRect(caret_location, caret_size); } -NGPhysicalOffsetRect ComputeLocalCaretRectAtTextOffset( +PhysicalRect ComputeLocalCaretRectAtTextOffset( const NGPaintFragment& paint_fragment, unsigned offset) { const auto& fragment = @@ -81,16 +80,15 @@ NGPhysicalOffsetRect ComputeLocalCaretRectAtTextOffset( } // Adjust the location to be relative to the inline formatting context. - NGPhysicalOffset caret_location = NGPhysicalOffset(caret_left, caret_top) + - paint_fragment.InlineOffsetToContainerBox(); - NGPhysicalSize caret_size(caret_width, caret_height); + PhysicalOffset caret_location = PhysicalOffset(caret_left, caret_top) + + paint_fragment.InlineOffsetToContainerBox(); + PhysicalSize caret_size(caret_width, caret_height); const NGPaintFragment& context_fragment = *NGPaintFragment::GetForInlineContainer(fragment.GetLayoutObject()); const NGPaintFragment* line_box = paint_fragment.ContainerLineBox(); - const NGPhysicalOffset line_box_offset = - line_box->InlineOffsetToContainerBox(); - const NGPhysicalOffsetRect line_box_rect(line_box_offset, line_box->Size()); + const PhysicalOffset line_box_offset = line_box->InlineOffsetToContainerBox(); + const PhysicalRect line_box_rect(line_box_offset, line_box->Size()); // For horizontal text, adjust the location in the x direction to ensure that // it completely falls in the union of line box and containing block, and @@ -102,7 +100,7 @@ NGPhysicalOffsetRect ComputeLocalCaretRectAtTextOffset( std::max(context_fragment.Size().width, line_box_rect.Right()); caret_location.left = std::min(caret_location.left, max_x - caret_width); caret_location.left = LayoutUnit(caret_location.left.Round()); - return NGPhysicalOffsetRect(caret_location, caret_size); + return PhysicalRect(caret_location, caret_size); } // Similar adjustment and rounding for vertical text. @@ -112,7 +110,7 @@ NGPhysicalOffsetRect ComputeLocalCaretRectAtTextOffset( std::max(context_fragment.Size().height, line_box_rect.Bottom()); caret_location.top = std::min(caret_location.top, max_y - caret_height); caret_location.top = LayoutUnit(caret_location.top.Round()); - return NGPhysicalOffsetRect(caret_location, caret_size); + return PhysicalRect(caret_location, caret_size); } LocalCaretRect ComputeLocalCaretRect(const NGCaretPosition& caret_position) { @@ -125,32 +123,21 @@ LocalCaretRect ComputeLocalCaretRect(const NGCaretPosition& caret_position) { case NGCaretPositionType::kBeforeBox: case NGCaretPositionType::kAfterBox: { DCHECK(fragment.PhysicalFragment().IsBox()); - const NGPhysicalOffsetRect fragment_local_rect = - ComputeLocalCaretRectByBoxSide(fragment, - caret_position.position_type); - return {layout_object, fragment_local_rect.ToLayoutRect()}; + const PhysicalRect fragment_local_rect = ComputeLocalCaretRectByBoxSide( + fragment, caret_position.position_type); + return {layout_object, fragment_local_rect}; } case NGCaretPositionType::kAtTextOffset: { DCHECK(fragment.PhysicalFragment().IsText()); DCHECK(caret_position.text_offset.has_value()); - const NGPhysicalOffsetRect caret_rect = ComputeLocalCaretRectAtTextOffset( + const PhysicalRect caret_rect = ComputeLocalCaretRectAtTextOffset( fragment, *caret_position.text_offset); - LayoutRect layout_rect = caret_rect.ToLayoutRect(); - - // For vertical-rl, convert to "flipped block-flow" coordinates space. - // See core/layout/README.md#coordinate-spaces for details. - if (fragment.Style().IsFlippedBlocksWritingMode()) { - const LayoutBlockFlow* container = - layout_object->ContainingNGBlockFlow(); - container->FlipForWritingMode(layout_rect); - } - - return {layout_object, layout_rect}; + return {layout_object, caret_rect}; } } NOTREACHED(); - return {layout_object, LayoutRect()}; + return {layout_object, PhysicalRect()}; } LocalCaretRect ComputeLocalSelectionRect( @@ -159,32 +146,21 @@ LocalCaretRect ComputeLocalSelectionRect( if (!caret_rect.layout_object) return caret_rect; - const LayoutObject* layout_object = caret_rect.layout_object; - const LayoutRect rect = caret_rect.rect; - const NGPaintFragment& fragment = *caret_position.fragment; const NGPaintFragment* line_box = fragment.ContainerLineBox(); // TODO(xiaochengh): We'll hit this DCHECK for caret in empty block if we // enable LayoutNG in contenteditable. DCHECK(line_box); + PhysicalRect rect = caret_rect.rect; if (fragment.Style().IsHorizontalWritingMode()) { - const LayoutUnit line_top = line_box->InlineOffsetToContainerBox().top; - const LayoutUnit line_height = line_box->Size().height; - return LocalCaretRect(layout_object, LayoutRect(rect.X(), line_top, - rect.Width(), line_height)); - } - - const LayoutUnit line_top = line_box->InlineOffsetToContainerBox().left; - const LayoutUnit line_height = line_box->Size().width; - LayoutRect layout_rect(line_top, rect.Y(), line_height, rect.Height()); - // For vertical-rl, convert to "flipped block-flow" coordinates space. - // See core/layout/README.md#coordinate-spaces for details. - if (fragment.Style().IsFlippedBlocksWritingMode()) { - const LayoutBlockFlow* container = layout_object->ContainingNGBlockFlow(); - container->FlipForWritingMode(layout_rect); + rect.SetY(line_box->InlineOffsetToContainerBox().top); + rect.SetHeight(line_box->Size().height); + } else { + rect.SetX(line_box->InlineOffsetToContainerBox().left); + rect.SetHeight(line_box->Size().width); } - return LocalCaretRect(layout_object, layout_rect); + return {caret_rect.layout_object, rect}; } } // namespace diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_dirty_lines.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_dirty_lines.cc new file mode 100644 index 00000000000..61a0ecf53b5 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_dirty_lines.cc @@ -0,0 +1,50 @@ +// Copyright 2019 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_dirty_lines.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_line_box_fragment.h" + +namespace blink { + +void NGDirtyLines::MarkLastFragment() { + if (last_fragment_) { + // Changes in this LayoutObject may affect the line that contains its + // previous object. Mark the line box that contains the last fragment + // of the previous object. + last_fragment_->LastForSameLayoutObject()->MarkContainingLineBoxDirty(); + } else { + // If there were no fragments so far in this pre-order traversal, mark + // the first line box dirty. + DCHECK(block_fragment_); + if (NGPaintFragment* first_line = block_fragment_->FirstLineBox()) + first_line->MarkLineBoxDirty(); + } +} + +void NGDirtyLines::MarkAtTextOffset(unsigned offset) { + for (NGPaintFragment* child : block_fragment_->Children()) { + // Only the first dirty line is relevant. + if (child->IsDirty()) + break; + + const auto* line = + DynamicTo<NGPhysicalLineBoxFragment>(child->PhysicalFragment()); + if (!line) + continue; + + const auto* break_token = To<NGInlineBreakToken>(line->BreakToken()); + DCHECK(break_token); + if (break_token->IsFinished()) + break; + + if (offset < break_token->TextOffset()) { + child->MarkLineBoxDirty(); + break; + } + } +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_dirty_lines.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_dirty_lines.h new file mode 100644 index 00000000000..5e97423cd95 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_dirty_lines.h @@ -0,0 +1,91 @@ +// Copyright 2019 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_DIRTY_LINES_H_ +#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_DIRTY_LINES_H_ + +#include "third_party/blink/renderer/core/core_export.h" +#include "third_party/blink/renderer/core/layout/layout_box.h" +#include "third_party/blink/renderer/core/layout/layout_inline.h" +#include "third_party/blink/renderer/core/layout/layout_text.h" +#include "third_party/blink/renderer/core/paint/ng/ng_paint_fragment.h" + +namespace blink { + +// This class computes dirty line boxes. +class CORE_EXPORT NGDirtyLines { + STACK_ALLOCATED(); + + public: + explicit NGDirtyLines(const NGPaintFragment* block_fragment) + : block_fragment_(block_fragment) { + DCHECK(block_fragment_); + } + + // Call |Handle*| functions for each object by traversing the LayoutObject + // tree in pre-order DFS. + // + // They return |true| when a dirty line was found. Because only the first + // dirty line is relevant, no further calls are necessary. + bool HandleText(LayoutText* layout_text) { + if (layout_text->SelfNeedsLayout()) { + MarkLastFragment(); + return true; + } + UpdateLastFragment(layout_text->FirstInlineFragment()); + return false; + } + + bool HandleInlineBox(LayoutInline* layout_inline) { + if (layout_inline->SelfNeedsLayout()) { + MarkLastFragment(); + return true; + } + // Do not keep fragments of LayoutInline unless it's a leaf, because + // the last fragment of LayoutInline is not the previous fragment of its + // descendants. + if (UNLIKELY(!layout_inline->FirstChild())) + UpdateLastFragment(layout_inline->FirstInlineFragment()); + return false; + } + + bool HandleAtomicInline(LayoutBox* layout_box) { + if (layout_box->NeedsLayout()) { + MarkLastFragment(); + return true; + } + UpdateLastFragment(layout_box->FirstInlineFragment()); + return false; + } + + bool HandleFloatingOrOutOfFlowPositioned(LayoutObject* layout_object) { + DCHECK(layout_object->IsFloatingOrOutOfFlowPositioned()); + if (layout_object->NeedsLayout()) { + MarkLastFragment(); + return true; + } + // Don't update last fragment. Floats and OOF are opaque. + return false; + } + + // Mark the line box at the specified text offset dirty. + void MarkAtTextOffset(unsigned offset); + + private: + void UpdateLastFragment(NGPaintFragment* fragment) { + if (fragment) + last_fragment_ = fragment; + } + + // Mark the line box that contains |last_fragment_| dirty. If |last_fragment_| + // is |nullptr|, the first line box is marked as dirty. + void MarkLastFragment(); + + const NGPaintFragment* block_fragment_; + NGPaintFragment* last_fragment_ = nullptr; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_DIRTY_LINES_H_ 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 1899fd16482..3fa122f9709 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 @@ -4,8 +4,8 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.h" -#include "third_party/blink/renderer/core/layout/ng/geometry/ng_logical_offset.h" -#include "third_party/blink/renderer/core/layout/ng/geometry/ng_logical_size.h" +#include "third_party/blink/renderer/core/layout/geometry/logical_offset.h" +#include "third_party/blink/renderer/core/layout/geometry/logical_size.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_line_box_fragment_builder.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.h" @@ -254,8 +254,8 @@ void NGInlineLayoutStateStack::AddBoxFragmentPlaceholder( DCHECK(box->style); const ComputedStyle& style = *box->style; - NGLogicalOffset offset; - NGLogicalSize size; + LogicalOffset offset; + LogicalSize size; if (!is_empty_line_) { // The inline box should have the height of the font metrics without the // line-height property. Compute from style because |box->metrics| includes @@ -394,12 +394,15 @@ unsigned NGInlineLayoutStateStack::UpdateBoxDataFragmentRange( const unsigned box_data_index = start->box_data_index; if (!box_data_index) continue; + // |box_data_list_[box_data_index - 1]| is the box for |start| child. + // Avoid keeping a pointer to the |BoxData| because it maybe invalidated as + // we add to |box_data_list_|. // As |box_data_index| is converted to start/end of BoxData, update // |box_data_index| to the parent box, or to 0 if no parent boxes. // This allows including this box to the nested parent box. - BoxData* box_data = &box_data_list_[box_data_index - 1]; - start->box_data_index = box_data->parent_box_data_index; + start->box_data_index = + box_data_list_[box_data_index - 1].parent_box_data_index; // Find the end line box item. const unsigned start_index = index; @@ -415,27 +418,33 @@ unsigned NGInlineLayoutStateStack::UpdateBoxDataFragmentRange( // because the update is limited only when its |box_data_index| is lower. while (end->box_data_index && end->box_data_index < box_data_index) { UpdateBoxDataFragmentRange(line_box, index); - // Re-compute |box_data| in case |box_data_list_| was reallocated when - // |UpdateBoxDataFragmentRange| added new fragments. - box_data = &box_data_list_[box_data_index - 1]; } if (box_data_index != end->box_data_index) break; - end->box_data_index = box_data->parent_box_data_index; + end->box_data_index = + box_data_list_[box_data_index - 1].parent_box_data_index; } // If this is the first range for this BoxData, set it. - if (!box_data->fragment_end) { - box_data->fragment_start = start_index; - box_data->fragment_end = index; + if (!box_data_list_[box_data_index - 1].fragment_end) { + box_data_list_[box_data_index - 1].SetFragmentRange(start_index, index); } else { // This box is fragmented by BiDi reordering. Add a new BoxData for the // fragmented range. - box_data->fragmented_box_data_index = box_data_list_.size(); - box_data_list_.emplace_back(*box_data, start_index, index); + box_data_list_[box_data_index - 1].fragmented_box_data_index = + box_data_list_.size(); + // Do not use `emplace_back()` here because adding to |box_data_list_| may + // reallocate the buffer, but the `BoxData` ctor must run before the + // reallocation. Create a new instance and |push_back()| instead. + BoxData fragmented_box_data(box_data_list_[box_data_index - 1], + start_index, index); + box_data_list_.push_back(fragmented_box_data); } - return box_data->parent_box_data_index ? start_index : index; + // If this box has parent boxes, we need to process it again. + if (box_data_list_[box_data_index - 1].parent_box_data_index) + return start_index; + return index; } return index; } @@ -572,10 +581,18 @@ NGInlineLayoutStateStack::BoxData::CreateBoxFragment( DCHECK(item); DCHECK(item->Style()); const ComputedStyle& style = *item->Style(); + + NGFragmentGeometry fragment_geometry; + fragment_geometry.border_box_size = size; + fragment_geometry.border_box_size.inline_size.ClampNegativeToZero(); + fragment_geometry.padding = + NGBoxStrut(padding, IsFlippedLinesWritingMode(style.GetWritingMode())); + // Because children are already in the visual order, use LTR for the // fragment builder so that it should not transform the coordinates for RTL. NGBoxFragmentBuilder box(item->GetLayoutObject(), &style, style.GetWritingMode(), TextDirection::kLtr); + box.SetInitialFragmentGeometry(fragment_geometry); box.SetBoxType(NGPhysicalFragment::kInlineBox); box.SetStyleVariant(item->StyleVariant()); @@ -583,14 +600,12 @@ NGInlineLayoutStateStack::BoxData::CreateBoxFragment( // was fragmented. Fragmenting a line box in block direction is not // supported today. box.SetBorderEdges({true, has_line_right_edge, true, has_line_left_edge}); - box.SetInlineSize(size.inline_size.ClampNegativeToZero()); - box.SetBlockSize(size.block_size); - box.SetPadding(padding); for (unsigned i = fragment_start; i < fragment_end; i++) { NGLineBoxFragmentBuilder::Child& child = (*line_box)[i]; if (child.layout_result) { - box.AddChild(*child.layout_result, child.offset - offset); + box.AddChild(child.layout_result->PhysicalFragment(), + child.offset - offset); child.layout_result.reset(); } else if (child.fragment) { box.AddChild(std::move(child.fragment), child.offset - offset); @@ -601,7 +616,7 @@ NGInlineLayoutStateStack::BoxData::CreateBoxFragment( // child.offset is the static position wrt. the linebox. As we are adding // this as a child of an inline level fragment, we adjust the static // position to be relative to this fragment. - NGLogicalOffset static_offset = child.offset - offset; + LogicalOffset static_offset = child.offset - offset; box.AddOutOfFlowChildCandidate(oof_box, static_offset, child.container_direction); @@ -609,6 +624,10 @@ NGInlineLayoutStateStack::BoxData::CreateBoxFragment( } } + // Inline boxes that produce DisplayItemClient should do full paint + // invalidations. + item->GetLayoutObject()->SetShouldDoFullPaintInvalidation(); + box.MoveOutOfFlowDescendantCandidatesToDescendants(); return box.ToInlineBoxFragment(); } @@ -747,10 +766,14 @@ NGLineHeightMetrics NGInlineLayoutStateStack::MetricsForTopAndBottomAlign( // BoxData contains inline boxes to be created later. Take them into account. for (const BoxData& box_data : box_data_list_) { + // |block_offset| is the top position when the baseline is at 0. LayoutUnit box_ascent = -line_box[box_data.fragment_end].offset.block_offset; - metrics.Unite( - NGLineHeightMetrics(box_ascent, box_data.size.block_size - box_ascent)); + LayoutUnit box_descent = box_data.size.block_size - box_ascent; + // The top/bottom of inline boxes should not include their paddings. + box_ascent -= box_data.padding.line_over; + box_descent -= box_data.padding.line_under; + metrics.Unite(NGLineHeightMetrics(box_ascent, box_descent)); } // In quirks mode, metrics is empty if no content. 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 f6ea8e18c77..2c6d9afb49d 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 @@ -5,7 +5,7 @@ #ifndef NGInlineBoxState_h #define NGInlineBoxState_h -#include "third_party/blink/renderer/core/layout/ng/geometry/ng_logical_size.h" +#include "third_party/blink/renderer/core/layout/geometry/logical_size.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment_builder.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.h" #include "third_party/blink/renderer/core/style/computed_style_constants.h" @@ -106,7 +106,7 @@ struct NGInlineBoxState { // 2) Performs layout when the positin/size of a box was computed. // 3) Cache common values for a box. class CORE_EXPORT NGInlineLayoutStateStack { - STACK_ALLOCATED(); + DISALLOW_NEW(); public: // The box state for the line box. @@ -204,7 +204,7 @@ class CORE_EXPORT NGInlineLayoutStateStack { BoxData(unsigned start, unsigned end, const NGInlineItem* item, - NGLogicalSize size) + LogicalSize size) : fragment_start(start), fragment_end(end), item(item), size(size) {} BoxData(const BoxData& other, unsigned start, unsigned end) @@ -214,12 +214,17 @@ class CORE_EXPORT NGInlineLayoutStateStack { size(other.size), offset(other.offset) {} + void SetFragmentRange(unsigned start_index, unsigned end_index) { + fragment_start = start_index; + fragment_end = end_index; + } + // The range of child fragments this box contains. unsigned fragment_start; unsigned fragment_end; const NGInlineItem* item; - NGLogicalSize size; + LogicalSize size; bool has_line_left_edge = false; bool has_line_right_edge = false; @@ -230,7 +235,7 @@ class CORE_EXPORT NGInlineLayoutStateStack { LayoutUnit margin_border_padding_line_left; LayoutUnit margin_border_padding_line_right; - NGLogicalOffset offset; + LogicalOffset offset; unsigned parent_box_data_index = 0; unsigned fragmented_box_data_index = 0; diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.cc index 23f9cdbfa28..3a9b6f7814d 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.cc @@ -41,7 +41,7 @@ NGInlineBreakToken::NGInlineBreakToken(NGLayoutInputNode node) NGInlineBreakToken::~NGInlineBreakToken() = default; -#ifndef NDEBUG +#if DCHECK_IS_ON() String NGInlineBreakToken::ToString() const { StringBuilder string_builder; @@ -55,6 +55,6 @@ String NGInlineBreakToken::ToString() const { return string_builder.ToString(); } -#endif // NDEBUG +#endif // DCHECK_IS_ON() } // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h index f457fe494e2..ba6599469b6 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h @@ -78,9 +78,9 @@ class CORE_EXPORT NGInlineBreakToken final : public NGBreakToken { void SetIgnoreFloats() { ignore_floats_ = true; } bool IgnoreFloats() const { return ignore_floats_; } -#ifndef NDEBUG +#if DCHECK_IS_ON() String ToString() const override; -#endif // NDEBUG +#endif private: NGInlineBreakToken(NGInlineNode node, diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_child_layout_context.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_child_layout_context.h index 059776134d3..5992007f223 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_child_layout_context.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_child_layout_context.h @@ -18,7 +18,7 @@ class NGInlineItem; // Because this context is in initial state for when fragmentation occurs and // some other cases, do not add things that are too expensive to rebuild. class NGInlineChildLayoutContext { - STACK_ALLOCATED(); + DISALLOW_NEW(); public: // Returns the NGInlineLayoutStateStack in this context. 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 9c4720ab64e..5e20fc2a028 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 @@ -72,7 +72,7 @@ class NGPhysicalFragmentCollectorBase { for (const auto& child : To<NGPhysicalContainerFragment>(fragment).Children()) { - base::AutoReset<NGPhysicalOffset> offset_resetter( + base::AutoReset<PhysicalOffset> offset_resetter( ¤t_offset_to_root_, current_offset_to_root_ + child.Offset()); base::AutoReset<const NGPhysicalFragment*> fragment_resetter( ¤t_fragment_, child.get()); @@ -86,7 +86,7 @@ class NGPhysicalFragmentCollectorBase { private: const NGPhysicalFragment* root_fragment_ = nullptr; const NGPhysicalFragment* current_fragment_ = nullptr; - NGPhysicalOffset current_offset_to_root_; + PhysicalOffset current_offset_to_root_; Vector<Result> results_; bool should_stop_traversing_ = false; 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 77cfcafd79e..801aaf305c2 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 @@ -110,6 +110,38 @@ unsigned NGLineInfo::InflowEndOffset() const { return StartOffset(); } +bool NGLineInfo::ShouldHangTrailingSpaces() const { + DCHECK(HasTrailingSpaces()); + if (!line_style_->AutoWrap()) + return false; + switch (text_align_) { + case ETextAlign::kStart: + case ETextAlign::kJustify: + return true; + case ETextAlign::kEnd: + case ETextAlign::kCenter: + case ETextAlign::kWebkitCenter: + return false; + case ETextAlign::kLeft: + case ETextAlign::kWebkitLeft: + return IsLtr(BaseDirection()); + case ETextAlign::kRight: + case ETextAlign::kWebkitRight: + return IsRtl(BaseDirection()); + } + NOTREACHED(); +} + +void NGLineInfo::UpdateTextAlign() { + text_align_ = line_style_->GetTextAlign(IsLastLine()); + + if (HasTrailingSpaces() && ShouldHangTrailingSpaces()) { + hang_width_ = ComputeTrailingSpaceWidth(&end_offset_for_justify_); + } else if (text_align_ == ETextAlign::kJustify) { + end_offset_for_justify_ = InflowEndOffset(); + } +} + LayoutUnit NGLineInfo::ComputeTrailingSpaceWidth( unsigned* end_offset_out) const { if (!has_trailing_spaces_) { 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 9e0253f842b..505bf23e4d6 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 @@ -176,17 +176,22 @@ class CORE_EXPORT NGLineInfo { void SetTextIndent(LayoutUnit indent) { text_indent_ = indent; } LayoutUnit TextIndent() const { return text_indent_; } + ETextAlign TextAlign() const { return text_align_; } + // Update |TextAlign()| and related fields. This depends on |IsLastLine()| and + // that must be called after |SetIsLastLine()|. + void UpdateTextAlign(); + NGBfcOffset BfcOffset() const { return bfc_offset_; } LayoutUnit AvailableWidth() const { return available_width_; } // The width of this line. Includes trailing spaces if they were preserved. // Negative width created by negative 'text-indent' is clamped to zero. LayoutUnit Width() const { return width_.ClampNegativeToZero(); } - // Same as |Width()| but returns negative value as is. - LayoutUnit WidthForAlignment() const { return width_; } - // The width of preserved trailing spaces. - LayoutUnit ComputeTrailingSpaceWidth( - unsigned* end_offset_out = nullptr) const; + // Same as |Width()| but returns negative value as is. Preserved trailing + // spaces may or may not be included, depends on |ShouldHangTrailingSpaces()|. + LayoutUnit WidthForAlignment() const { return width_ - hang_width_; } + // Width that hangs over the end of the line; e.g., preserved trailing spaces. + LayoutUnit HangWidth() const { return hang_width_; } // Compute |Width()| from |Results()|. Used during line breaking, before // |Width()| is set. After line breaking, this should match to |Width()| // without clamping. @@ -194,6 +199,7 @@ class CORE_EXPORT NGLineInfo { bool HasTrailingSpaces() const { return has_trailing_spaces_; } void SetHasTrailingSpaces() { has_trailing_spaces_ = true; } + bool ShouldHangTrailingSpaces() const; // True if this line has overflow, excluding preserved trailing spaces. bool HasOverflow() const { return has_overflow_; } @@ -211,6 +217,12 @@ class CORE_EXPORT NGLineInfo { // End text offset of this line, excluding out-of-flow objects such as // floating or positioned. unsigned InflowEndOffset() const; + // End text offset for `text-align: justify`. This excludes preserved trailing + // spaces. Available only when |TextAlign()| is |kJustify|. + unsigned EndOffsetForJustify() const { + DCHECK_EQ(text_align_, ETextAlign::kJustify); + return end_offset_for_justify_; + } // End item index of this line. unsigned EndItemIndex() const { return end_item_index_; } void SetEndItemIndex(unsigned index) { end_item_index_ = index; } @@ -234,6 +246,10 @@ class CORE_EXPORT NGLineInfo { private: bool ComputeNeedsAccurateEndPosition() const; + // The width of preserved trailing spaces. + LayoutUnit ComputeTrailingSpaceWidth( + unsigned* end_offset_out = nullptr) const; + const NGInlineItemsData* items_data_ = nullptr; const ComputedStyle* line_style_ = nullptr; NGInlineItemResults results_; @@ -243,11 +259,14 @@ class CORE_EXPORT NGLineInfo { LayoutUnit available_width_; LayoutUnit width_; + LayoutUnit hang_width_; LayoutUnit text_indent_; unsigned start_offset_; unsigned end_item_index_; + unsigned end_offset_for_justify_; + ETextAlign text_align_ = ETextAlign::kLeft; TextDirection base_direction_ = TextDirection::kLtr; bool use_first_line_style_ = false; 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 d05f2ce5e12..e881df5256a 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 @@ -9,7 +9,9 @@ #include "third_party/blink/renderer/core/layout/layout_inline.h" #include "third_party/blink/renderer/core/layout/layout_text.h" #include "third_party/blink/renderer/core/layout/ng/inline/layout_ng_text.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_dirty_lines.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping_builder.h" #include "third_party/blink/renderer/core/style/computed_style.h" #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_view.h" @@ -150,6 +152,8 @@ inline bool IsCollapsibleSpace(UChar c) { // It makes the line breaker easier to handle. inline bool IsControlItemCharacter(UChar c) { return c == kNewlineCharacter || c == kTabulationCharacter || + // Make ZWNJ a control character so that it can prevent kerning. + c == kZeroWidthNonJoinerCharacter || // Include ignorable character here to avoids shaping/rendering // these glyphs, and to help the line breaker to ignore them. ShouldIgnore(c); @@ -275,7 +279,7 @@ void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>:: template <typename OffsetMappingBuilder> bool NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendTextReusing( - const String& original_string, + const NGInlineNodeData& original_data, LayoutText* layout_text) { DCHECK(layout_text); const NGInlineItems& items = layout_text->InlineItems(); @@ -283,6 +287,8 @@ bool NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendTextReusing( if (!old_item0.Length()) return false; + const String& original_string = original_data.text_content; + // Don't reuse existing items if they might be affected by whitespace // collapsing. // TODO(layout-dev): This could likely be optimized further. @@ -311,12 +317,26 @@ bool NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendTextReusing( } break; case NGInlineItem::kNotCollapsible: { - // If the start of the original string was collapsed, it may be - // restored. const String& source_text = layout_text->GetText(); - if (source_text.length() && IsCollapsibleSpace(source_text[0]) && - original_string[old_item0.StartOffset()] != kSpaceCharacter) - return false; + if (source_text.length() && IsCollapsibleSpace(source_text[0])) { + // If the start of the original string was collapsed, it may be + // restored. + if (original_string[old_item0.StartOffset()] != kSpaceCharacter) + return false; + // If the start of the original string was not collapsed, and the + // collapsible space run contains newline, the newline may be + // removed. + unsigned offset = 0; + UChar c = source_text[0]; + bool contains_newline = + MoveToEndOfCollapsibleSpaces(source_text, &offset, &c); + if (contains_newline && + ShouldRemoveNewline(text_, text_.length(), last_item->Style(), + StringView(source_text, offset), + &new_style)) { + return false; + } + } break; } case NGInlineItem::kCollapsed: @@ -340,12 +360,22 @@ bool NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendTextReusing( return false; } - if (bidi_context_.size() && new_style.PreserveNewline()) { - // We exit and then re-enter all bidi contexts around a forced breaks. We + if (new_style.PreserveNewline()) { + // We exit and then re-enter all bidi contexts around a forced break. So, We // must go through the full pipeline to ensure that we exit and enter the - // contexts in the same in the re-layout. - if (layout_text->GetText().Contains(kNewlineCharacter)) - return false; + // correct bidi contexts the re-layout. + if (bidi_context_.size() || layout_text->HasBidiControlInlineItems()) { + if (layout_text->GetText().Contains(kNewlineCharacter)) + return false; + } + } + + if (UNLIKELY(old_item0.StartOffset() > 0 && + ShouldInsertBreakOpportunityAfterLeadingPreservedSpaces( + layout_text->GetText(), new_style))) { + // e.g. <p>abc xyz</p> => <p> xyz</p> where "abc" and " xyz" are different + // Text node. |text_| is " \u200Bxyz". + return false; } for (const NGInlineItem& item : items) { @@ -398,7 +428,7 @@ bool NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendTextReusing( template <> bool NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::AppendTextReusing( - const String&, + const NGInlineNodeData&, LayoutText*) { NOTREACHED(); return false; @@ -406,6 +436,30 @@ bool NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::AppendTextReusing( template <typename OffsetMappingBuilder> void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendText( + LayoutText* layout_text, + const NGInlineNodeData* previous_data) { + // Mark dirty lines. Clear if marked, only the first dirty line is relevant. + if (dirty_lines_ && dirty_lines_->HandleText(layout_text)) + dirty_lines_ = nullptr; + + // If the LayoutText element hasn't changed, reuse the existing items. + if (previous_data && layout_text->HasValidInlineItems()) { + if (AppendTextReusing(*previous_data, layout_text)) { + return; + } + } + + // If not create a new item as needed. + if (UNLIKELY(layout_text->IsWordBreak())) { + AppendBreakOpportunity(layout_text); + return; + } + + AppendText(layout_text->GetText(), layout_text); +} + +template <typename OffsetMappingBuilder> +void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendText( const String& string, LayoutText* layout_object) { DCHECK(layout_object); @@ -623,6 +677,18 @@ void NGInlineItemsBuilderTemplate< is_empty_inline_ = false; // text item is not empty. } +template <typename OffsetMappingBuilder> +bool NGInlineItemsBuilderTemplate<OffsetMappingBuilder>:: + ShouldInsertBreakOpportunityAfterLeadingPreservedSpaces( + const String& string, + const ComputedStyle& style) const { + return text_.IsEmpty() && string.length() > 0 && + string[0] == kSpaceCharacter && !style.CollapseWhiteSpace() && + style.AutoWrap(); +} + +// TODO(yosin): We should remove |style| and |string| parameter because of +// except for testing, we can get them from |LayoutText|. // Even when without whitespace collapsing, control characters (newlines and // tabs) are in their own control items to make the line breaker not special. template <typename OffsetMappingBuilder> @@ -638,8 +704,8 @@ void NGInlineItemsBuilderTemplate< // opportunity after leading preserved spaces needs a special code in the line // breaker. Generate an opportunity to make it easy. unsigned start = 0; - if (UNLIKELY(text_.IsEmpty() && string[start] == kSpaceCharacter && - style->AutoWrap())) { + if (UNLIKELY(ShouldInsertBreakOpportunityAfterLeadingPreservedSpaces( + string, *style))) { do { ++start; } while (start < string.length() && string[start] == kSpaceCharacter); @@ -653,7 +719,9 @@ void NGInlineItemsBuilderTemplate< if (c == kNewlineCharacter) { AppendForcedBreak(layout_object); start++; - } else if (c == kTabulationCharacter) { + continue; + } + if (c == kTabulationCharacter) { wtf_size_t end = string.Find( [](UChar c) { return c != kTabulationCharacter; }, start + 1); if (end == kNotFound) @@ -661,11 +729,14 @@ void NGInlineItemsBuilderTemplate< AppendTextItem(NGInlineItem::kControl, StringView(string, start, end - start), layout_object); start = end; - } else { + continue; + } + // ZWNJ splits item, but it should be text. + if (c != kZeroWidthNonJoinerCharacter) { Append(NGInlineItem::kControl, c, layout_object); start++; + continue; } - continue; } wtf_size_t end = string.Find(IsControlItemCharacter, start + 1); @@ -773,6 +844,11 @@ void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendAtomicInline( Append(NGInlineItem::kAtomicInline, kObjectReplacementCharacter, layout_object); + // Mark dirty lines. Clear if marked, only the first dirty line is relevant. + if (dirty_lines_ && + dirty_lines_->HandleAtomicInline(ToLayoutBox(layout_object))) + dirty_lines_ = nullptr; + // When this atomic inline is inside of an inline box, the height of the // inline box can be different from the height of the atomic inline. Ensure // the inline box creates a box fragment so that its height is available in @@ -787,17 +863,25 @@ void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendAtomicInline( template <typename OffsetMappingBuilder> void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::AppendFloating( LayoutObject* layout_object) { - changes_may_affect_earlier_lines_ = true; AppendOpaque(NGInlineItem::kFloating, kObjectReplacementCharacter, layout_object); + + // Mark dirty lines. Clear if marked, only the first dirty line is relevant. + if (dirty_lines_ && + dirty_lines_->HandleFloatingOrOutOfFlowPositioned(layout_object)) + dirty_lines_ = nullptr; } template <typename OffsetMappingBuilder> void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>:: AppendOutOfFlowPositioned(LayoutObject* layout_object) { - changes_may_affect_earlier_lines_ = true; AppendOpaque(NGInlineItem::kOutOfFlowPositioned, kObjectReplacementCharacter, layout_object); + + // Mark dirty lines. Clear if marked, only the first dirty line is relevant. + if (dirty_lines_ && + dirty_lines_->HandleFloatingOrOutOfFlowPositioned(layout_object)) + dirty_lines_ = nullptr; } template <typename OffsetMappingBuilder> @@ -857,6 +941,12 @@ void NGInlineItemsBuilderTemplate< text_.erase(space_offset); mapping_builder_.CollapseTrailingSpace(space_offset); + // Mark dirty lines. Clear if marked, only the first dirty line is relevant. + if (dirty_lines_) { + dirty_lines_->MarkAtTextOffset(space_offset); + dirty_lines_ = nullptr; + } + // Keep the item even if the length became zero. This is not needed for // the layout purposes, but needed to maintain LayoutObject states. See // |AddEmptyTextItem()|. @@ -972,7 +1062,7 @@ void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::EnterBlock( template <typename OffsetMappingBuilder> void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::EnterInline( - LayoutObject* node) { + LayoutInline* node) { DCHECK(node); // https://drafts.csswg.org/css-writing-modes-3/#bidi-control-codes-injection-table @@ -1013,6 +1103,10 @@ void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::EnterInline( AppendOpaque(NGInlineItem::kOpenTag, node); + // Mark dirty lines. Clear if marked, only the first dirty line is relevant. + if (dirty_lines_ && dirty_lines_->HandleInlineBox(node)) + dirty_lines_ = nullptr; + if (!NeedsBoxInfo()) return; @@ -1072,7 +1166,8 @@ void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::SetIsSymbolMarker( template <typename OffsetMappingBuilder> void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::ClearInlineFragment( LayoutObject* object) { - NGInlineNode::ClearInlineFragment(object); + object->SetIsInLayoutNGInlineFormattingContext(true); + object->SetFirstInlineFragment(nullptr); } template <typename OffsetMappingBuilder> 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 9d04bba9549..b7c9530edd1 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 @@ -21,6 +21,8 @@ class ComputedStyle; class LayoutInline; class LayoutObject; class LayoutText; +struct NGInlineNodeData; +class NGDirtyLines; // NGInlineItemsBuilder builds a string and a list of NGInlineItem from inlines. // @@ -42,8 +44,13 @@ class NGInlineItemsBuilderTemplate { STACK_ALLOCATED(); public: - explicit NGInlineItemsBuilderTemplate(Vector<NGInlineItem>* items) - : items_(items) {} + // Create a builder that appends items to |items|. + // + // If |dirty_lines| is given, this builder calls its functions to mark lines + // dirty. + explicit NGInlineItemsBuilderTemplate(Vector<NGInlineItem>* items, + NGDirtyLines* dirty_lines = nullptr) + : items_(items), dirty_lines_(dirty_lines) {} ~NGInlineItemsBuilderTemplate(); String ToString(); @@ -62,11 +69,19 @@ class NGInlineItemsBuilderTemplate { return changes_may_affect_earlier_lines_; } + // Append a string from |LayoutText|. + // + // If |previous_data| is given, reuse existing items if they exist and are + // reusable. Otherwise appends new items. + void AppendText(LayoutText* layout_text, + const NGInlineNodeData* previous_data); + // Append existing items from an unchanged LayoutObject. // Returns whether the existing items could be reused. // NOTE: The state of the builder remains unchanged if the append operation // fails (i.e. if it returns false). - bool AppendTextReusing(const String& previous_text, LayoutText* layout_text); + bool AppendTextReusing(const NGInlineNodeData& previous_data, + LayoutText* layout_text); // Append a string. // When appending, spaces are collapsed according to CSS Text, The white space @@ -114,7 +129,7 @@ class NGInlineItemsBuilderTemplate { void EnterBlock(const ComputedStyle*); void ExitBlock(); - void EnterInline(LayoutObject*); + void EnterInline(LayoutInline*); void ExitInline(LayoutObject*); OffsetMappingBuilder& GetOffsetMappingBuilder() { return mapping_builder_; } @@ -134,6 +149,8 @@ class NGInlineItemsBuilderTemplate { Vector<NGInlineItem>* items_; StringBuilder text_; + NGDirtyLines* dirty_lines_; + // |mapping_builder_| builds the whitespace-collapsed offset mapping // during inline collection. It is updated whenever |text_| is modified or a // white space is collapsed. @@ -200,11 +217,17 @@ class NGInlineItemsBuilderTemplate { void AppendGeneratedBreakOpportunity(LayoutObject*); void Exit(LayoutObject*); + + bool ShouldInsertBreakOpportunityAfterLeadingPreservedSpaces( + const String&, + const ComputedStyle&) const; }; template <> -CORE_EXPORT bool NGInlineItemsBuilderTemplate< - NGOffsetMappingBuilder>::AppendTextReusing(const String&, LayoutText*); +CORE_EXPORT bool +NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::AppendTextReusing( + const NGInlineNodeData&, + LayoutText*); template <> CORE_EXPORT void 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 868bbe90a35..56fa24daadc 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 @@ -7,13 +7,12 @@ #include "testing/gtest/include/gtest/gtest.h" #include "third_party/blink/renderer/core/layout/layout_inline.h" #include "third_party/blink/renderer/core/layout/ng/inline/layout_ng_text.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.h" #include "third_party/blink/renderer/core/layout/ng/ng_layout_test.h" #include "third_party/blink/renderer/core/style/computed_style.h" namespace blink { -namespace { - // The spec turned into a discussion that may change. Put this logic on hold // until CSSWG resolves the issue. // https://github.com/w3c/csswg-drafts/issues/337 @@ -79,7 +78,7 @@ class NGInlineItemsBuilderTest : public NGLayoutTest { builder.ExitBlock(); text_ = builder.ToString(); ValidateItems(); - CheckReuseItemsProducesSameResult(inputs); + CheckReuseItemsProducesSameResult(inputs, builder.HasBidiControls()); for (LayoutObject* anonymous_object : anonymous_objects) anonymous_object->Destroy(); return text_; @@ -111,7 +110,12 @@ class NGInlineItemsBuilderTest : public NGLayoutTest { EXPECT_EQ(current_offset, text_.length()); } - void CheckReuseItemsProducesSameResult(Vector<Input> inputs) { + void CheckReuseItemsProducesSameResult(Vector<Input> inputs, + bool has_bidi_controls) { + NGInlineNodeData fake_data; + fake_data.text_content = text_; + fake_data.is_bidi_enabled_ = has_bidi_controls; + Vector<NGInlineItem> reuse_items; NGInlineItemsBuilder reuse_builder(&reuse_items); for (Input& input : inputs) { @@ -131,8 +135,9 @@ class NGInlineItemsBuilderTest : public NGLayoutTest { } // Try to re-use previous items, or Append if it was not re-usable. - bool reused = input.layout_text->HasValidInlineItems() && - reuse_builder.AppendTextReusing(text_, input.layout_text); + bool reused = + input.layout_text->HasValidInlineItems() && + reuse_builder.AppendTextReusing(fake_data, input.layout_text); if (!reused) { reuse_builder.AppendText(input.text, input.layout_text); } @@ -493,6 +498,4 @@ TEST_F(NGInlineItemsBuilderTest, BidiIsolateOverride) { builder.ToString()); } -} // namespace - } // namespace blink 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 c8c01e84f7c..f3d4a644b8c 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 @@ -96,8 +96,10 @@ NGInlineBoxState* NGInlineLayoutAlgorithm::HandleCloseTag( box->EnsureTextMetrics(*item.Style(), baseline_type_); box = box_states_->OnCloseTag(&line_box_, box, baseline_type_, item.HasEndEdge()); - item.GetLayoutObject()->SetShouldDoFullPaintInvalidation(); - ClearNeedsLayoutIfNeeded(item.GetLayoutObject()); + // Just clear |NeedsLayout| flags. Culled inline boxes do not need paint + // invalidations. If this object produces box fragments, + // |NGInlineBoxStateStack| takes care of invalidations. + item.GetLayoutObject()->ClearNeedsLayoutWithoutPaintInvalidation(); return box; } @@ -239,7 +241,8 @@ void NGInlineLayoutAlgorithm::CreateLine( } line_box_.AddChild(text_builder.ToTextFragment(), box->text_top, item_result.inline_size, item.BidiLevel()); - ClearNeedsLayoutIfNeeded(item.GetLayoutObject()); + // Text boxes always need full paint invalidations. + item.GetLayoutObject()->ClearNeedsLayoutWithFullPaintInvalidation(); } else if (item.Type() == NGInlineItem::kControl) { PlaceControlItem(item, *line_info, &item_result, box); } else if (item.Type() == NGInlineItem::kOpenTag) { @@ -290,6 +293,14 @@ void NGInlineLayoutAlgorithm::CreateLine( box_states_->UpdateAfterReorder(&line_box_); } LayoutUnit inline_size = box_states_->ComputeInlinePositions(&line_box_); + if (LayoutUnit hang_width = line_info->HangWidth()) { + inline_size -= hang_width; + container_builder_.SetHangInlineSize(hang_width); + + if (IsRtl(line_info->BaseDirection())) { + line_box_.MoveInInlineDirection(-hang_width); + } + } // Truncate the line if 'text-overflow: ellipsis' is set. if (UNLIKELY(inline_size > line_info->AvailableWidth() && @@ -346,6 +357,7 @@ void NGInlineLayoutAlgorithm::CreateLine( // 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()) { + container_builder_.SetIsSelfCollapsing(); container_builder_.SetIsEmptyLineBox(); container_builder_.SetBaseDirection(line_info->BaseDirection()); container_builder_.AddChildren(line_box_); @@ -413,7 +425,7 @@ void NGInlineLayoutAlgorithm::PlaceControlItem(const NGInlineItem& item, // Place a generated content that does not exist in DOM nor in LayoutObject // tree. void NGInlineLayoutAlgorithm::PlaceGeneratedContent( - scoped_refptr<const NGPhysicalFragment> fragment, + scoped_refptr<const NGPhysicalTextFragment> fragment, UBiDiLevel bidi_level, NGInlineBoxState* box) { LayoutUnit inline_size = IsHorizontalWritingMode() ? fragment->Size().width @@ -466,7 +478,7 @@ void NGInlineLayoutAlgorithm::PlaceLayoutResult(NGInlineItemResult* item_result, NGBoxFragment fragment(ConstraintSpace().GetWritingMode(), ConstraintSpace().Direction(), To<NGPhysicalBoxFragment>( - *item_result->layout_result->PhysicalFragment())); + item_result->layout_result->PhysicalFragment())); NGLineHeightMetrics metrics = fragment.BaselineMetrics( {NGBaselineAlgorithmType::kAtomicInline, baseline_type_}, ConstraintSpace()); @@ -475,7 +487,7 @@ void NGInlineLayoutAlgorithm::PlaceLayoutResult(NGInlineItemResult* item_result, LayoutUnit line_top = item_result->margins.line_over - metrics.ascent; line_box_.AddChild(std::move(item_result->layout_result), - NGLogicalOffset{inline_offset, line_top}, + LogicalOffset{inline_offset, line_top}, item_result->inline_size, item.BidiLevel()); } @@ -486,6 +498,8 @@ void NGInlineLayoutAlgorithm::PlaceOutOfFlowObjects( DCHECK(line_info.IsEmptyLine() || !line_box_metrics.IsEmpty()) << "Non-empty lines must have a valid set of linebox metrics."; + bool is_empty_inline = Node().IsEmptyInline(); + // All children within the linebox are positioned relative to the baseline, // then shifted later using NGLineBoxFragmentBuilder::MoveInBlockDirection. LayoutUnit baseline_adjustment = @@ -516,25 +530,34 @@ void NGInlineLayoutAlgorithm::PlaceOutOfFlowObjects( // To correctly determine which "line" block-level out-of-flow positioned // object is placed on, we need to keep track of if there is any inline-level // content preceeding it. - bool has_preceeding_inline_level_content = false; + bool has_preceding_inline_level_content = false; for (NGLineBoxFragmentBuilder::Child& child : line_box_) { - has_preceeding_inline_level_content |= child.HasInFlowFragment(); + has_preceding_inline_level_content |= child.HasInFlowFragment(); LayoutObject* box = child.out_of_flow_positioned_box; if (!box) continue; - NGLogicalOffset static_offset(LayoutUnit(), baseline_adjustment); + LogicalOffset static_offset(LayoutUnit(), baseline_adjustment); if (box->StyleRef().IsOriginalDisplayInlineType()) { // An inline-level OOF element positions itself within the line, at the // position it would have been if it was in-flow. static_offset.inline_offset = child.offset.inline_offset; + + // The static-position of inline-level OOF-positioned nodes depends on + // previous floats (if any). + // + // If we are an empty-inline we may not have the correct BFC block-offset + // yet. Due to this we need to mark this node as having adjoining + // objects, and perform a re-layout if our position shifts. + if (is_empty_inline) + container_builder_.AddAdjoiningFloatTypes(kAdjoiningInlineOutOfFlow); } else { // A block-level OOF element positions itself on the "next" line. However // only shifts down if there is inline-level content. static_offset.inline_offset = block_level_inline_offset; - if (has_preceeding_inline_level_content) + if (has_preceding_inline_level_content) static_offset.block_offset += line_height; } @@ -566,9 +589,8 @@ void NGInlineLayoutAlgorithm::PlaceFloatingObjects( bool is_empty_inline = Node().IsEmptyInline(); LayoutUnit bfc_block_offset = line_info.BfcOffset().block_offset; - if (is_empty_inline && ConstraintSpace().FloatsBfcBlockOffset()) { - bfc_block_offset = *ConstraintSpace().FloatsBfcBlockOffset(); - } + if (is_empty_inline && ConstraintSpace().ForcedBfcBlockOffset()) + bfc_block_offset = *ConstraintSpace().ForcedBfcBlockOffset(); LayoutUnit bfc_line_offset = container_builder_.BfcLineOffset(); @@ -587,7 +609,7 @@ void NGInlineLayoutAlgorithm::PlaceFloatingObjects( // Skip any children which aren't positioned floats. if (!child.layout_result || - !child.layout_result->PhysicalFragment()->IsFloating()) + !child.layout_result->PhysicalFragment().IsFloating()) continue; LayoutUnit block_offset = @@ -595,9 +617,8 @@ void NGInlineLayoutAlgorithm::PlaceFloatingObjects( // We need to manually account for the flipped-lines writing mode here :(. if (IsFlippedLinesWritingMode(ConstraintSpace().GetWritingMode())) { - NGFragment fragment( - ConstraintSpace().GetWritingMode(), - To<NGPhysicalBoxFragment>(*child.layout_result->PhysicalFragment())); + NGFragment fragment(ConstraintSpace().GetWritingMode(), + child.layout_result->PhysicalFragment()); block_offset = -fragment.BlockSize() - block_offset; } @@ -629,10 +650,7 @@ bool NGInlineLayoutAlgorithm::ApplyJustify(LayoutUnit space, return false; // Justify the end of visible text, ignoring preserved trailing spaces. - unsigned end_offset; - LayoutUnit trailing_spaces_width = - line_info->ComputeTrailingSpaceWidth(&end_offset); - space += trailing_spaces_width; + unsigned end_offset = line_info->EndOffsetForJustify(); // If this line overflows, fallback to 'text-align: start'. if (space <= 0) @@ -700,8 +718,7 @@ LayoutUnit NGInlineLayoutAlgorithm::ApplyTextAlign(NGLineInfo* line_info) { LayoutUnit space = line_info->AvailableWidth() - line_info->WidthForAlignment(); - const ComputedStyle& line_style = line_info->LineStyle(); - ETextAlign text_align = line_style.GetTextAlign(line_info->IsLastLine()); + ETextAlign text_align = line_info->TextAlign(); if (text_align == ETextAlign::kJustify) { // If justification succeeds, no offset is needed. Expansions are set to // each |NGInlineItemResult| in |line_info|. @@ -781,16 +798,22 @@ scoped_refptr<const NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { unsigned handled_leading_floats_index = PositionLeadingFloats(&initial_exclusion_space, &leading_floats); + // Only empty-inlines should have the "forced" BFC block-offset set. + DCHECK(is_empty_inline || !ConstraintSpace().ForcedBfcBlockOffset()); + // 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 LayoutOpportunityVector opportunities = initial_exclusion_space.AllLayoutOpportunities( - ConstraintSpace().BfcOffset(), + {ConstraintSpace().BfcOffset().line_offset, + ConstraintSpace().ForcedBfcBlockOffset().value_or( + ConstraintSpace().BfcOffset().block_offset)}, ConstraintSpace().AvailableSize().inline_size); NGExclusionSpace exclusion_space; const NGInlineBreakToken* break_token = BreakToken(); + bool is_line_created = false; LayoutUnit line_block_size; LayoutUnit block_delta; const auto* opportunities_it = opportunities.begin(); @@ -818,6 +841,7 @@ scoped_refptr<const NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { // Reset any state that may have been modified in a previous pass. container_builder_.Reset(); exclusion_space = initial_exclusion_space; + is_line_created = false; NGLineLayoutOpportunity line_opportunity = opportunity.ComputeLineLayoutOpportunity(ConstraintSpace(), @@ -834,8 +858,8 @@ scoped_refptr<const NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { // *and* the opportunity is smaller than the available inline-size, and the // container autowraps, continue to the next opportunity. if (line_info.HasOverflow() && - ConstraintSpace().AvailableSize().inline_size != - line_opportunity.AvailableFloatInlineSize() && + !line_opportunity.IsEqualToAvailableFloatInlineSize( + ConstraintSpace().AvailableSize().inline_size) && Node().Style().AutoWrap()) { // Shapes are *special*. We need to potentially increment the block-delta // by 1px each loop to properly test each potential position of the line. @@ -851,11 +875,15 @@ scoped_refptr<const NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { line_block_size = LayoutUnit(); ++opportunities_it; } + // There must be at least one more opportunity, or we fail to call + // |CreateLine()|. + DCHECK_NE(opportunities_it, opportunities.end()); continue; } PrepareBoxStates(line_info, break_token); CreateLine(line_opportunity, &line_info, &exclusion_space); + is_line_created = true; // We now can check the block-size of the fragment, and it fits within the // opportunity. @@ -894,10 +922,6 @@ scoped_refptr<const NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { continue; } - if (opportunity.rect.BlockStartOffset() > - ConstraintSpace().BfcOffset().block_offset) - container_builder_.SetIsPushedByFloats(); - // Success! container_builder_.SetBreakToken(line_breaker.CreateBreakToken(line_info)); @@ -909,10 +933,20 @@ scoped_refptr<const NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { // TODO(ikilpatrick): Move this into ng_block_layout_algorithm. container_builder_.SetBlockSize( ComputeContentSize(line_info, exclusion_space, line_height)); + + // As we aren't an empty inline we should have correctly placed all + // our adjoining floats, and shouldn't propagate this information + // to siblings. + container_builder_.ResetAdjoiningFloatTypes(); + + if (opportunity.rect.BlockStartOffset() > + ConstraintSpace().BfcOffset().block_offset) + container_builder_.SetIsPushedByFloats(); } break; } + CHECK(is_line_created); container_builder_.SetExclusionSpace(std::move(exclusion_space)); container_builder_.MoveOutOfFlowDescendantCandidatesToDescendants(); return container_builder_.ToLineBoxFragment(); @@ -948,13 +982,13 @@ unsigned NGInlineLayoutAlgorithm::PositionLeadingFloats( ? kFloatTypeLeft : kFloatTypeRight); - // If we are an empty inline, and don't have the special floats BFC + // If we are an empty inline, and don't have the special forced BFC // block-offset yet, there is no way to position any floats. - if (is_empty_inline && !ConstraintSpace().FloatsBfcBlockOffset()) + if (is_empty_inline && !ConstraintSpace().ForcedBfcBlockOffset()) continue; LayoutUnit origin_bfc_block_offset = - is_empty_inline ? *ConstraintSpace().FloatsBfcBlockOffset() + is_empty_inline ? *ConstraintSpace().ForcedBfcBlockOffset() : ConstraintSpace().BfcOffset().block_offset; NGPositionedFloat positioned_float = PositionFloat( 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 e12d473e761..d93ab75c097 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 @@ -80,7 +80,7 @@ class CORE_EXPORT NGInlineLayoutAlgorithm final const NGLineInfo&, NGInlineItemResult*, NGInlineBoxState*); - void PlaceGeneratedContent(scoped_refptr<const NGPhysicalFragment>, + void PlaceGeneratedContent(scoped_refptr<const NGPhysicalTextFragment>, UBiDiLevel, NGInlineBoxState*); NGInlineBoxState* PlaceAtomicInline(const NGInlineItem&, 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 f1d65e70440..1c01a0a125e 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 @@ -41,7 +41,7 @@ TEST_F(NGInlineLayoutAlgorithmTest, BreakToken) { auto* block_flow = To<LayoutBlockFlow>(GetLayoutObjectByElementId("container")); NGInlineNode inline_node(block_flow); - NGLogicalSize size(LayoutUnit(50), LayoutUnit(20)); + LogicalSize size(LayoutUnit(50), LayoutUnit(20)); NGConstraintSpace constraint_space = NGConstraintSpaceBuilder( @@ -53,23 +53,20 @@ TEST_F(NGInlineLayoutAlgorithmTest, BreakToken) { NGInlineChildLayoutContext context; scoped_refptr<const NGLayoutResult> layout_result = inline_node.Layout(constraint_space, nullptr, &context); - auto* line1 = - To<NGPhysicalLineBoxFragment>(layout_result->PhysicalFragment()); - EXPECT_FALSE(line1->BreakToken()->IsFinished()); + const auto& line1 = layout_result->PhysicalFragment(); + EXPECT_FALSE(line1.BreakToken()->IsFinished()); // Perform 2nd layout with the break token from the 1st line. scoped_refptr<const NGLayoutResult> layout_result2 = - inline_node.Layout(constraint_space, line1->BreakToken(), &context); - auto* line2 = - To<NGPhysicalLineBoxFragment>(layout_result2->PhysicalFragment()); - EXPECT_FALSE(line2->BreakToken()->IsFinished()); + inline_node.Layout(constraint_space, line1.BreakToken(), &context); + const auto& line2 = layout_result2->PhysicalFragment(); + EXPECT_FALSE(line2.BreakToken()->IsFinished()); // Perform 3rd layout with the break token from the 2nd line. scoped_refptr<const NGLayoutResult> layout_result3 = - inline_node.Layout(constraint_space, line2->BreakToken(), &context); - auto* line3 = - To<NGPhysicalLineBoxFragment>(layout_result3->PhysicalFragment()); - EXPECT_TRUE(line3->BreakToken()->IsFinished()); + inline_node.Layout(constraint_space, line2.BreakToken(), &context); + const auto& line3 = layout_result3->PhysicalFragment(); + EXPECT_TRUE(line3.BreakToken()->IsFinished()); } TEST_F(NGInlineLayoutAlgorithmTest, GenerateHyphen) { @@ -168,10 +165,10 @@ TEST_F(NGInlineLayoutAlgorithmTest, To<NGPhysicalLineBoxFragment>(*container->Children()[0]); EXPECT_EQ(1u, linebox.Children().size()); - EXPECT_EQ(NGPhysicalSize(), linebox.Size()); + EXPECT_EQ(PhysicalSize(), linebox.Size()); const auto& oof_container = To<NGPhysicalBoxFragment>(*linebox.Children()[0]); - EXPECT_EQ(NGPhysicalSize(), oof_container.Size()); + EXPECT_EQ(PhysicalSize(), oof_container.Size()); } // This test ensures that if an inline box generates (or does not generate) box @@ -234,13 +231,12 @@ TEST_F(NGInlineLayoutAlgorithmTest, ContainerBorderPadding) { NGConstraintSpace::CreateFromLayoutObject(*block_flow); scoped_refptr<const NGLayoutResult> layout_result = block_node.Layout(space); - auto* block_box = - To<NGPhysicalBoxFragment>(layout_result->PhysicalFragment()); EXPECT_TRUE(layout_result->BfcBlockOffset().has_value()); EXPECT_EQ(0, *layout_result->BfcBlockOffset()); EXPECT_EQ(0, layout_result->BfcLineOffset()); - NGPhysicalOffset line_offset = block_box->Children()[0].Offset(); + PhysicalOffset line_offset = + layout_result->PhysicalFragment().Children()[0].Offset(); EXPECT_EQ(5, line_offset.left); EXPECT_EQ(10, line_offset.top); } @@ -270,9 +266,9 @@ TEST_F(NGInlineLayoutAlgorithmTest, MAYBE_VerticalAlignBottomReplaced) { scoped_refptr<const NGLayoutResult> layout_result = inline_node.Layout(space, nullptr, &context); - auto* line = To<NGPhysicalLineBoxFragment>(layout_result->PhysicalFragment()); - EXPECT_EQ(LayoutUnit(96), line->Size().height); - NGPhysicalOffset img_offset = line->Children()[0].Offset(); + const auto& line = layout_result->PhysicalFragment(); + EXPECT_EQ(LayoutUnit(96), line.Size().height); + PhysicalOffset img_offset = line.Children()[0].Offset(); EXPECT_EQ(LayoutUnit(0), img_offset.top); } @@ -316,7 +312,7 @@ TEST_F(NGInlineLayoutAlgorithmTest, TextFloatsAroundFloatsBefore) { To<NGPhysicalBoxFragment>(html_fragment->Children()[0].get()); auto* container_fragment = To<NGPhysicalBoxFragment>(body_fragment->Children()[0].get()); - Vector<NGPhysicalOffset> line_offsets; + Vector<PhysicalOffset> line_offsets; for (const auto& child : container_fragment->Children()) { if (!child->IsLineBox()) continue; @@ -368,7 +364,7 @@ TEST_F(NGInlineLayoutAlgorithmTest, TextFloatsAroundInlineFloatThatFitsOnLine) { // Two lines. EXPECT_EQ(2u, block_box->Children().size()); - NGPhysicalOffset first_line_offset = block_box->Children()[1].Offset(); + PhysicalOffset first_line_offset = block_box->Children()[1].Offset(); // 30 == narrow-float's width. EXPECT_EQ(LayoutUnit(30), first_line_offset.left); @@ -500,7 +496,7 @@ TEST_F(NGInlineLayoutAlgorithmTest, InkOverflow) { EXPECT_EQ(LayoutUnit(10), box_fragment.Size().height); - NGPhysicalOffsetRect ink_overflow = paint_fragment->InkOverflow(); + PhysicalRect ink_overflow = paint_fragment->InkOverflow(); EXPECT_EQ(LayoutUnit(-5), ink_overflow.offset.top); EXPECT_EQ(LayoutUnit(20), ink_overflow.size.height); 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 e2e32f01d1b..846eb8c7ef1 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 @@ -7,6 +7,7 @@ #include <algorithm> #include <memory> +#include "build/build_config.h" #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_list_marker.h" @@ -15,6 +16,7 @@ #include "third_party/blink/renderer/core/layout/logical_values.h" #include "third_party/blink/renderer/core/layout/ng/inline/layout_ng_text.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_bidi_paragraph.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_dirty_lines.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_item.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_items_builder.h" @@ -62,83 +64,28 @@ unsigned EstimateOffsetMappingItemsCount(const LayoutBlockFlow& block) { return EstimateInlineItemsCount(block) / 4; } -// This class marks appropriate line box fragments as dirty. -// -// |CollectInlinesInternal| calls this class when traversing the LayoutObject -// tree in pre-order DFS -class NGLineBoxMarker { - STACK_ALLOCATED(); - - public: - NGLineBoxMarker(NGPaintFragment* block_fragment) - : block_fragment_(block_fragment) { - DCHECK(block_fragment_); - } - - bool HandleText(LayoutText* layout_text) { - if (layout_text->SelfNeedsLayout()) - return Mark(); - return UpdateLastFragment(layout_text->FirstInlineFragment()); - } - - bool HandleInlineBox(LayoutInline* layout_inline) { - if (layout_inline->SelfNeedsLayout()) - return Mark(); - - // Do not keep fragments of LayoutInline unless it's a leaf, because - // the last fragment of LayoutInline is not the previous fragment of its - // descendants. - if (layout_inline->FirstChild()) - return false; - return UpdateLastFragment(layout_inline->FirstInlineFragment()); - } - - bool HandleAtomicInline(LayoutBox* layout_box) { - if (layout_box->NeedsLayout()) - return Mark(); - return UpdateLastFragment(layout_box->FirstInlineFragment()); - } - - private: - bool Mark() { - if (last_fragment_) { - // Changes in this LayoutObject may affect the line that contains its - // previous object. Mark the line box that contains the last fragment - // of the previous object. - last_fragment_->LastForSameLayoutObject()->MarkContainingLineBoxDirty(); - } else { - // If there were no fragments so far in this pre-order traversal, mark - // the first line box dirty. - DCHECK(block_fragment_); - if (NGPaintFragment* first_line = block_fragment_->FirstLineBox()) - first_line->MarkLineBoxDirty(); - } - return true; - } - - bool UpdateLastFragment(NGPaintFragment* fragment) { - if (fragment) - last_fragment_ = fragment; - return false; - } - - NGPaintFragment* block_fragment_; - NGPaintFragment* last_fragment_ = nullptr; -}; - // This class has the same interface as NGInlineItemsBuilder but does nothing // except tracking if floating or out-of-flow objects are added. // // |MarkLineBoxesDirty| uses this class to traverse tree without buildling // |NGInlineItem|. class ItemsBuilderForMarkLineBoxesDirty { + STACK_ALLOCATED(); + public: - void AppendText(const String&, LayoutText*) {} - bool AppendTextReusing(const String&, LayoutText*) { return false; } + ItemsBuilderForMarkLineBoxesDirty(NGDirtyLines* dirty_lines) + : dirty_lines_(dirty_lines) {} + void AppendText(LayoutText* layout_text, const NGInlineItemsData*) { + if (dirty_lines_ && dirty_lines_->HandleText(layout_text)) + dirty_lines_ = nullptr; + } void AppendOpaque(NGInlineItem::NGInlineItemType, LayoutObject*) {} - void AppendBreakOpportunity(LayoutObject*) {} - void AppendAtomicInline(LayoutObject*) {} + void AppendAtomicInline(LayoutObject* layout_object) { + if (dirty_lines_ && + dirty_lines_->HandleAtomicInline(ToLayoutBox(layout_object))) + dirty_lines_ = nullptr; + } void AppendFloating(LayoutObject*) { has_floating_or_out_of_flow_positioned_ = true; } @@ -148,7 +95,10 @@ class ItemsBuilderForMarkLineBoxesDirty { void SetIsSymbolMarker(bool) {} void EnterBlock(const ComputedStyle*) {} void ExitBlock() {} - void EnterInline(LayoutObject*) {} + void EnterInline(LayoutInline* layout_inline) { + if (dirty_lines_ && dirty_lines_->HandleInlineBox(layout_inline)) + dirty_lines_ = nullptr; + } void ExitInline(LayoutObject*) {} bool ShouldAbort() const { @@ -164,12 +114,12 @@ class ItemsBuilderForMarkLineBoxesDirty { } void ClearInlineFragment(LayoutObject* object) { - NGInlineNode::ClearInlineFragment(object); + DCHECK(object->IsInLayoutNGInlineFormattingContext()); } void ClearNeedsLayout(LayoutObject* object) { object->ClearNeedsLayout(); - object->ClearNeedsCollectInlines(); + DCHECK(!object->NeedsCollectInlines()); ClearInlineFragment(object); } @@ -178,6 +128,7 @@ class ItemsBuilderForMarkLineBoxesDirty { } private: + NGDirtyLines* dirty_lines_; bool has_floating_or_out_of_flow_positioned_ = false; }; @@ -194,8 +145,7 @@ class ItemsBuilderForMarkLineBoxesDirty { template <typename ItemsBuilder> void CollectInlinesInternal(LayoutBlockFlow* block, ItemsBuilder* builder, - String* previous_text, - NGLineBoxMarker* marker) { + const NGInlineNodeData* previous_data) { builder->EnterBlock(block->Style()); LayoutObject* node = GetLayoutObjectForFirstChildNode(block); @@ -203,28 +153,11 @@ void CollectInlinesInternal(LayoutBlockFlow* block, LayoutNGListItem::FindSymbolMarkerLayoutText(block); while (node) { if (LayoutText* layout_text = ToLayoutTextOrNull(node)) { - // If the LayoutText element hasn't changed, reuse the existing items. - - // if the last ended with space and this starts with space, do not allow - // reuse. builder->MightCollapseWithPreceding(*previous_text) - bool item_reused = false; - if (previous_text && layout_text->HasValidInlineItems()) - item_reused = builder->AppendTextReusing(*previous_text, layout_text); - - // If not create a new item as needed. - if (!item_reused) { - if (UNLIKELY(layout_text->IsWordBreak())) - builder->AppendBreakOpportunity(layout_text); - else - builder->AppendText(layout_text->GetText(), layout_text); - } + builder->AppendText(layout_text, previous_data); if (symbol == layout_text) builder->SetIsSymbolMarker(true); - if (marker && marker->HandleText(layout_text)) - marker = nullptr; - builder->ClearNeedsLayout(layout_text); } else if (node->IsFloating()) { @@ -252,9 +185,6 @@ void CollectInlinesInternal(LayoutBlockFlow* block, // signal the presence of a non-text object to the unicode bidi // algorithm. builder->AppendAtomicInline(node); - - if (marker && marker->HandleAtomicInline(ToLayoutBox(node))) - marker = nullptr; } builder->ClearInlineFragment(node); @@ -268,9 +198,6 @@ void CollectInlinesInternal(LayoutBlockFlow* block, builder->EnterInline(layout_inline); - if (marker && marker->HandleInlineBox(layout_inline)) - marker = nullptr; - // Traverse to children if they exist. if (LayoutObject* child = layout_inline->FirstChild()) { node = child; @@ -302,7 +229,65 @@ void CollectInlinesInternal(LayoutBlockFlow* block, builder->ExitBlock(); } -static bool NeedsShaping(const NGInlineItem& item) { +// Returns whether this text should break shaping. Even within a box, text runs +// that have different shaping properties need to break shaping. +inline bool ShouldBreakShapingBeforeText(const NGInlineItem& item, + const NGInlineItem& start_item, + const ComputedStyle& start_style, + const Font& start_font, + TextDirection start_direction) { + DCHECK_EQ(item.Type(), NGInlineItem::kText); + DCHECK(item.Style()); + const ComputedStyle& style = *item.Style(); + if (&style != &start_style) { + const Font& font = style.GetFont(); + if (&font != &start_font && font != start_font) + return true; + } + + // The resolved direction and run segment properties must match to shape + // across for HarfBuzzShaper. + return item.Direction() != start_direction || + !item.EqualsRunSegment(start_item); +} + +// Returns whether the start of this box should break shaping. +inline bool ShouldBreakShapingBeforeBox(const NGInlineItem& item, + const Font& start_font) { + DCHECK_EQ(item.Type(), NGInlineItem::kOpenTag); + DCHECK(item.Style()); + const ComputedStyle& style = *item.Style(); + + // These properties values must break shaping. + // https://drafts.csswg.org/css-text-3/#boundary-shaping + if ((style.MayHavePadding() && !style.PaddingStart().IsZero()) || + (style.MayHaveMargin() && !style.MarginStart().IsZero()) || + style.BorderStartWidth() || + style.VerticalAlign() != EVerticalAlign::kBaseline) + return true; + + return false; +} + +// Returns whether the end of this box should break shaping. +inline bool ShouldBreakShapingAfterBox(const NGInlineItem& item, + const Font& start_font) { + DCHECK_EQ(item.Type(), NGInlineItem::kCloseTag); + DCHECK(item.Style()); + const ComputedStyle& style = *item.Style(); + + // These properties values must break shaping. + // https://drafts.csswg.org/css-text-3/#boundary-shaping + if ((style.MayHavePadding() && !style.PaddingEnd().IsZero()) || + (style.MayHaveMargin() && !style.MarginEnd().IsZero()) || + style.BorderEndWidth() || + style.VerticalAlign() != EVerticalAlign::kBaseline) + return true; + + return false; +} + +inline bool NeedsShaping(const NGInlineItem& item) { return item.Type() == NGInlineItem::kText && !item.TextShapeResult(); } @@ -363,17 +348,31 @@ void NGInlineNode::PrepareLayoutIfNeeded() { block_flow->ResetNGInlineNodeData(); } + if (RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) { + if (const NGPaintFragment* fragment = block_flow->PaintFragment()) { + NGDirtyLines dirty_lines(fragment); + PrepareLayout(std::move(previous_data), &dirty_lines); + return; + } + } + PrepareLayout(std::move(previous_data), /* dirty_lines */ nullptr); +} + +void NGInlineNode::PrepareLayout( + std::unique_ptr<NGInlineNodeData> previous_data, + NGDirtyLines* dirty_lines) { // Scan list of siblings collecting all in-flow non-atomic inlines. A single // NGInlineNode represent a collection of adjacent non-atomic inlines. NGInlineNodeData* data = MutableData(); DCHECK(data); - CollectInlines(data, previous_data.get()); + CollectInlines(data, previous_data.get(), dirty_lines); SegmentText(data); ShapeText(data, previous_data.get()); ShapeTextForFirstLineIfNeeded(data); AssociateItemsWithInlines(data); DCHECK_EQ(data, MutableData()); + LayoutBlockFlow* block_flow = GetLayoutBlockFlow(); block_flow->ClearNeedsCollectInlines(); #if DCHECK_IS_ON() @@ -418,7 +417,7 @@ void NGInlineNode::ComputeOffsetMapping(LayoutBlockFlow* layout_block_flow, NGInlineItemsBuilderForOffsetMapping builder(&items); builder.GetOffsetMappingBuilder().ReserveCapacity( EstimateOffsetMappingItemsCount(*layout_block_flow)); - CollectInlinesInternal(layout_block_flow, &builder, nullptr, nullptr); + CollectInlinesInternal(layout_block_flow, &builder, nullptr); // For non-NG object, we need the text, and also the inline items to resolve // bidi levels. Otherwise |data| already has the text from the pre-layout @@ -443,6 +442,14 @@ const NGOffsetMapping* NGInlineNode::GetOffsetMapping( LayoutBlockFlow* layout_block_flow) { DCHECK(!layout_block_flow->GetDocument().NeedsLayoutTreeUpdate()); + if (UNLIKELY(layout_block_flow->NeedsLayout())) { + // TODO(kojii): This shouldn't happen, but is not easy to fix all cases. + // Return nullptr so that callers can chose to fail gracefully, or + // null-deref. crbug.com/946004 + NOTREACHED(); + return nullptr; + } + // If |layout_block_flow| is LayoutNG, compute from |NGInlineNode|. if (layout_block_flow->IsLayoutNGMixin()) { NGInlineNode node(layout_block_flow); @@ -466,23 +473,16 @@ const NGOffsetMapping* NGInlineNode::GetOffsetMapping( // parent LayoutInline where possible, and joining all text content in a single // string to allow bidi resolution and shaping of the entire block. void NGInlineNode::CollectInlines(NGInlineNodeData* data, - NGInlineNodeData* previous_data) { + NGInlineNodeData* previous_data, + NGDirtyLines* dirty_lines) { DCHECK(data->text_content.IsNull()); DCHECK(data->items.IsEmpty()); LayoutBlockFlow* block = GetLayoutBlockFlow(); block->WillCollectInlines(); - // If we have PaintFragment, mark line boxes dirty from |NeedsLayout| flag. - base::Optional<NGLineBoxMarker> marker; - if (NGPaintFragment* block_fragment = block->PaintFragment()) - marker.emplace(block_fragment); - - String* previous_text = - previous_data ? &previous_data->text_content : nullptr; data->items.ReserveCapacity(EstimateInlineItemsCount(*block)); - NGInlineItemsBuilder builder(&data->items); - CollectInlinesInternal(block, &builder, previous_text, - marker.has_value() ? &*marker : nullptr); + NGInlineItemsBuilder builder(&data->items, dirty_lines); + CollectInlinesInternal(block, &builder, previous_data); data->text_content = builder.ToString(); // Set |is_bidi_enabled_| for all UTF-16 strings for now, because at this @@ -678,22 +678,27 @@ void NGInlineNode::ShapeText(NGInlineItemsData* data, if (item.Type() == NGInlineItem::kText) { if (!item.Length()) continue; - // 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() || - !item.EqualsRunSegment(start_item)) + if (ShouldBreakShapingBeforeText(item, start_item, start_style, font, + direction)) { + break; + } + // Break shaping at ZWNJ so that it prevents kerning. ZWNJ is always at + // the beginning of an item for this purpose; see NGInlineItemsBuilder. + if (text_content[item.StartOffset()] == kZeroWidthNonJoinerCharacter) break; end_offset = item.EndOffset(); num_text_items++; - } else if (item.Type() == NGInlineItem::kOpenTag || - item.Type() == NGInlineItem::kCloseTag) { - // These items are opaque to shaping. - // Opaque items cannot have text, such as Object Replacement Characters, - // since such characters can affect shaping. + } else if (item.Type() == NGInlineItem::kOpenTag) { + if (ShouldBreakShapingBeforeBox(item, font)) { + break; + } + // Should not have any characters to be opaque to shaping. + DCHECK_EQ(0u, item.Length()); + } else if (item.Type() == NGInlineItem::kCloseTag) { + if (ShouldBreakShapingAfterBox(item, font)) { + break; + } + // Should not have any characters to be opaque to shaping. DCHECK_EQ(0u, item.Length()); } else { break; @@ -841,22 +846,6 @@ void NGInlineNode::ShapeTextForFirstLineIfNeeded(NGInlineNodeData* data) { item.SetStyleVariant(NGStyleVariant::kFirstLine); } - // 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.SetShouldCreateBoxFragment(); - } - 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()); @@ -876,12 +865,18 @@ void NGInlineNode::AssociateItemsWithInlines(NGInlineNodeData* data) { // Items split from a LayoutObject should be consecutive. DCHECK(associated_objects.insert(object).is_new_entry); #endif + layout_text->ClearHasBidiControlInlineItems(); + bool has_bidi_control = false; NGInlineItem* begin = item; for (++item; item != items.end(); ++item) { if (item->GetLayoutObject() != object) break; + if (item->Type() == NGInlineItem::kBidiControl) + has_bidi_control = true; } layout_text->SetInlineItems(begin, item); + if (has_bidi_control) + layout_text->SetHasBidiControlInlineItems(); continue; } ++item; @@ -891,7 +886,7 @@ void NGInlineNode::AssociateItemsWithInlines(NGInlineNodeData* data) { void NGInlineNode::ClearAssociatedFragments( const NGPhysicalFragment& fragment, const NGBlockBreakToken* block_break_token) { - auto* block_flow = To<LayoutBlockFlow>(fragment.GetLayoutObject()); + auto* block_flow = To<LayoutBlockFlow>(fragment.GetMutableLayoutObject()); if (!block_flow->ChildrenInline()) return; NGInlineNode node = NGInlineNode(block_flow); @@ -949,11 +944,28 @@ scoped_refptr<const NGLayoutResult> NGInlineNode::Layout( const auto* inline_break_token = To<NGInlineBreakToken>(break_token); NGInlineLayoutAlgorithm algorithm(*this, constraint_space, inline_break_token, context); - return algorithm.Layout(); + auto layout_result = algorithm.Layout(); + +#if defined(OS_ANDROID) + // Cached position data is crucial for line breaking performance and is + // preserved across layouts to speed up subsequent layout passes due to + // reflow, page zoom, window resize, etc. On Android though reflows are less + // common, page zoom isn't used (instead uses pinch-zoom), and the window + // typically can't be resized (apart from rotation). To reduce memory usage + // discard the cached position data after layout. + NGInlineNodeData* data = MutableData(); + for (auto& item : data->items) { + if (item.shape_result_) + item.shape_result_->DiscardPositionData(); + } +#endif // defined(OS_ANDROID) + + return layout_result; } const NGPaintFragment* NGInlineNode::ReusableLineBoxContainer( const NGConstraintSpace& constraint_space) { + DCHECK(RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()); // |SelfNeedsLayout()| is the most common reason that we check it earlier. LayoutBlockFlow* block_flow = GetLayoutBlockFlow(); DCHECK(!block_flow->SelfNeedsLayout()); @@ -987,7 +999,8 @@ const NGPaintFragment* NGInlineNode::ReusableLineBoxContainer( return nullptr; // Propagating OOF needs re-layout. - if (!cached_layout_result->OutOfFlowPositionedDescendants().IsEmpty()) + if (cached_layout_result->PhysicalFragment() + .HasOutOfFlowPositionedDescendants()) return nullptr; // Cached fragments are not for intermediate layout. @@ -1002,11 +1015,9 @@ const NGPaintFragment* NGInlineNode::ReusableLineBoxContainer( if (!paint_fragment) return nullptr; - if (!MarkLineBoxesDirty(block_flow)) + if (!MarkLineBoxesDirty(block_flow, paint_fragment)) return nullptr; - PrepareLayoutIfNeeded(); - if (Data().changes_may_affect_earlier_lines_) return nullptr; @@ -1018,10 +1029,19 @@ const NGPaintFragment* NGInlineNode::ReusableLineBoxContainer( // Removals of LayoutObject already marks relevant line boxes dirty by calling // |DirtyLinesFromChangedChild()|, but insertions and style changes are not // marked yet. -bool NGInlineNode::MarkLineBoxesDirty(LayoutBlockFlow* block_flow) { - NGLineBoxMarker marker(block_flow->PaintFragment()); - ItemsBuilderForMarkLineBoxesDirty builder; - CollectInlinesInternal(block_flow, &builder, nullptr, &marker); +bool NGInlineNode::MarkLineBoxesDirty(LayoutBlockFlow* block_flow, + const NGPaintFragment* paint_fragment) { + DCHECK(RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()); + NGDirtyLines dirty_lines(paint_fragment); + if (block_flow->NeedsCollectInlines()) { + std::unique_ptr<NGInlineNodeData> previous_data; + previous_data.reset(block_flow->TakeNGInlineNodeData()); + block_flow->ResetNGInlineNodeData(); + PrepareLayout(std::move(previous_data), &dirty_lines); + return true; + } + ItemsBuilderForMarkLineBoxesDirty builder(&dirty_lines); + CollectInlinesInternal(block_flow, &builder, nullptr); return !builder.ShouldAbort(); } @@ -1042,7 +1062,7 @@ static LayoutUnit ComputeContentSize( /* out_writing_mode */ writing_mode, /* is_new_fc */ false) .SetTextDirection(style.Direction()) - .SetAvailableSize({available_inline_size, NGSizeIndefinite}) + .SetAvailableSize({available_inline_size, kIndefiniteSize}) .SetPercentageResolutionSize({LayoutUnit(), LayoutUnit()}) .SetReplacedPercentageResolutionSize({LayoutUnit(), LayoutUnit()}) .SetIsIntermediateLayout(true) 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 94052b21d64..a3bcb07a50b 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 @@ -9,6 +9,7 @@ #include "third_party/blink/renderer/core/layout/layout_block_flow.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.h" #include "third_party/blink/renderer/core/layout/ng/ng_layout_input_node.h" +#include "third_party/blink/renderer/core/paint/ng/ng_paint_fragment.h" #include "third_party/blink/renderer/platform/wtf/casting.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" @@ -16,10 +17,11 @@ namespace blink { class NGBlockBreakToken; class NGConstraintSpace; +class NGDirtyLines; class NGInlineChildLayoutContext; +class NGInlineNodeLegacy; class NGLayoutResult; class NGOffsetMapping; -class NGInlineNodeLegacy; struct MinMaxSize; struct NGInlineItemsData; @@ -59,8 +61,11 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode { // Instruct to re-compute |PrepareLayout| on the next layout. void InvalidatePrepareLayoutForTest() { - GetLayoutBlockFlow()->ResetNGInlineNodeData(); + LayoutBlockFlow* block_flow = GetLayoutBlockFlow(); + block_flow->ResetNGInlineNodeData(); DCHECK(!IsPrepareLayoutFinished()); + // There shouldn't be paint fragment if NGInlineNodeData does not exist. + block_flow->SetPaintFragment(nullptr, nullptr); } const NGInlineItemsData& ItemsData(bool is_first_line) const { @@ -100,21 +105,18 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode { String ToString() const; - // A helper function for NGInlineItemsBuilder. - static void ClearInlineFragment(LayoutObject* object) { - object->SetIsInLayoutNGInlineFormattingContext(true); - object->SetFirstInlineFragment(nullptr); - } - protected: bool IsPrepareLayoutFinished() const; // Prepare inline and text content for layout. Must be called before // calling the Layout method. void PrepareLayoutIfNeeded(); + void PrepareLayout(std::unique_ptr<NGInlineNodeData> previous_data, + NGDirtyLines* dirty_lines); void CollectInlines(NGInlineNodeData*, - NGInlineNodeData* previous_data = nullptr); + NGInlineNodeData* previous_data = nullptr, + NGDirtyLines* dirty_lines = nullptr); void SegmentText(NGInlineNodeData*); void SegmentScriptRuns(NGInlineNodeData*); void SegmentFontOrientation(NGInlineNodeData*); @@ -124,7 +126,7 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode { void ShapeTextForFirstLineIfNeeded(NGInlineNodeData*); void AssociateItemsWithInlines(NGInlineNodeData*); - bool MarkLineBoxesDirty(LayoutBlockFlow*); + bool MarkLineBoxesDirty(LayoutBlockFlow*, const NGPaintFragment*); NGInlineNodeData* MutableData() { return To<LayoutBlockFlow>(box_)->GetNGInlineNodeData(); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.h index 64c19d1af7b..833d63944c7 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.h @@ -19,6 +19,8 @@ struct CORE_EXPORT NGInlineNodeData : NGInlineItemsData { return static_cast<TextDirection>(base_direction_); } + bool IsEmptyInline() const { return is_empty_inline_; } + private: const NGInlineItemsData& ItemsData(bool is_first_line) const { return !is_first_line || !first_line_items_ @@ -29,6 +31,7 @@ struct CORE_EXPORT NGInlineNodeData : NGInlineItemsData { base_direction_ = static_cast<unsigned>(direction); } + friend class NGInlineItemsBuilderTest; friend class NGInlineNode; friend class NGInlineNodeLegacy; friend class NGInlineNodeForTest; 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 2851a3b14ec..4ee6e041cba 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 @@ -21,6 +21,7 @@ #include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h" #include "third_party/blink/renderer/core/paint/ng/ng_paint_fragment.h" #include "third_party/blink/renderer/core/style/computed_style.h" +#include "third_party/blink/renderer/core/svg_names.h" namespace blink { @@ -77,7 +78,9 @@ class NGInlineNodeForTest : public NGInlineNode { void ShapeText() { NGInlineNode::ShapeText(MutableData()); } bool MarkLineBoxesDirty() { - return NGInlineNode::MarkLineBoxesDirty(GetLayoutBlockFlow()); + LayoutBlockFlow* block_flow = GetLayoutBlockFlow(); + return NGInlineNode::MarkLineBoxesDirty(block_flow, + block_flow->PaintFragment()); } }; @@ -132,9 +135,9 @@ class NGInlineNodeTest : public NGLayoutTest { nullptr /* break_token */, &context) .Layout(); - const auto* line = + const auto& line = To<NGPhysicalLineBoxFragment>(result->PhysicalFragment()); - for (const auto& child : line->Children()) { + for (const auto& child : line.Children()) { fragments_out->push_back(To<NGPhysicalTextFragment>(child.get())); } } @@ -546,7 +549,7 @@ TEST_F(NGInlineNodeTest, NeedsCollectInlinesOnSetText) { Element* container = GetElementById("container"); Element* parent = GetElementById("parent"); - Text* text = ToText(parent->firstChild()); + auto* text = To<Text>(parent->firstChild()); EXPECT_FALSE(text->GetLayoutObject()->NeedsCollectInlines()); EXPECT_FALSE(parent->GetLayoutObject()->NeedsCollectInlines()); EXPECT_FALSE(container->GetLayoutObject()->NeedsCollectInlines()); @@ -579,28 +582,35 @@ struct StyleChangeData { kAll = kText | kParentAndAbove, }; unsigned needs_collect_inlines; + base::Optional<bool> is_line_dirty; } style_change_data[] = { // Changing color, text-decoration, etc. should not re-run // |CollectInlines()|. - {"#parent.after { color: red; }", StyleChangeData::kNone}, + {"#parent.after { color: red; }", StyleChangeData::kNone, false}, {"#parent.after { text-decoration-line: underline; }", - StyleChangeData::kNone}, + StyleChangeData::kNone, false}, // Changing fonts should re-run |CollectInlines()|. - {"#parent.after { font-size: 200%; }", StyleChangeData::kAll}, + {"#parent.after { font-size: 200%; }", StyleChangeData::kAll, true}, + // Changing from/to out-of-flow should re-rerun |CollectInlines()|. + {"#parent.after { position: absolute; }", StyleChangeData::kContainer, + true}, + {"#parent { position: absolute; }" + "#parent.after { position: initial; }", + StyleChangeData::kContainer, true}, // List markers are captured in |NGInlineItem|. {"#parent.after { display: list-item; }", StyleChangeData::kContainer}, {"#parent { display: list-item; list-style-type: none; }" "#parent.after { list-style-type: disc; }", - StyleChangeData::kTextAndParent}, + StyleChangeData::kParent}, {"#parent { display: list-item; }" "#container.after { list-style-type: none; }", - StyleChangeData::kTextAndParent}, + StyleChangeData::kParent}, // Changing properties related with bidi resolution should re-run // |CollectInlines()|. {"#parent.after { unicode-bidi: bidi-override; }", - StyleChangeData::kParentAndAbove}, + StyleChangeData::kParentAndAbove, true}, {"#container.after { unicode-bidi: bidi-override; }", - StyleChangeData::kContainer}, + StyleChangeData::kContainer, false}, }; std::ostream& operator<<(std::ostream& os, const StyleChangeData& data) { @@ -630,7 +640,7 @@ TEST_P(StyleChangeTest, NeedsCollectInlinesOnStyle) { Element* container = GetElementById("container"); Element* parent = GetElementById("parent"); - Text* text = ToText(parent->firstChild()); + auto* text = To<Text>(parent->firstChild()); EXPECT_FALSE(text->GetLayoutObject()->NeedsCollectInlines()); EXPECT_FALSE(parent->GetLayoutObject()->NeedsCollectInlines()); EXPECT_FALSE(container->GetLayoutObject()->NeedsCollectInlines()); @@ -654,6 +664,15 @@ TEST_P(StyleChangeTest, NeedsCollectInlinesOnStyle) { Element* next = GetElementById("next"); EXPECT_FALSE(previous->GetLayoutObject()->NeedsCollectInlines()); EXPECT_FALSE(next->GetLayoutObject()->NeedsCollectInlines()); + + if (data.is_line_dirty && + RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) { + layout_block_flow_ = ToLayoutNGBlockFlow(container->GetLayoutObject()); + auto lines = MarkLineBoxesDirty(); + EXPECT_EQ(*data.is_line_dirty, lines[0]->IsDirty()); + } + + ForceLayout(); // Ensure running layout does not crash. } using CreateNode = Node* (*)(Document&); @@ -964,47 +983,88 @@ TEST_F(NGInlineNodeTest, SpaceRestoredByInsertingWord) { } // Test marking line boxes when inserting a span before the first child. -TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnInsert) { +TEST_P(NodeInsertTest, MarkLineBoxesDirtyOnInsert) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; SetupHtml("container", R"HTML( + <style> + .abspos { position: absolute; } + .float { float: left; } + </style> <div id=container style="font-size: 10px; width: 10ch"> 12345678 </div> )HTML"); - Element* span = GetDocument().CreateElementForBinding("span"); + Node* insert = (*GetParam())(GetDocument()); Element* container = GetElementById("container"); - container->insertBefore(span, container->firstChild()); + container->insertBefore(insert, container->firstChild()); auto lines = MarkLineBoxesDirty(); EXPECT_TRUE(lines[0]->IsDirty()); } // Test marking line boxes when appending a span. -TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnAppend) { +TEST_P(NodeInsertTest, MarkLineBoxesDirtyOnAppend) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; SetupHtml("container", R"HTML( + <style> + .abspos { position: absolute; } + .float { float: left; } + </style> <div id=container style="font-size: 10px; width: 10ch"> 12345678 </div> )HTML"); - Element* span = GetDocument().CreateElementForBinding("span"); - layout_block_flow_->GetNode()->appendChild(span); + Node* insert = (*GetParam())(GetDocument()); + layout_block_flow_->GetNode()->appendChild(insert); auto lines = MarkLineBoxesDirty(); EXPECT_TRUE(lines[0]->IsDirty()); } // Test marking line boxes when appending a span on 2nd line. -TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnAppend2) { +TEST_P(NodeInsertTest, MarkLineBoxesDirtyOnAppend2) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; SetupHtml("container", R"HTML( + <style> + .abspos { position: absolute; } + .float { float: left; } + </style> <div id=container style="font-size: 10px; width: 10ch"> 12345678 2234 </div> )HTML"); - Element* span = GetDocument().CreateElementForBinding("span"); - layout_block_flow_->GetNode()->appendChild(span); + Node* insert = (*GetParam())(GetDocument()); + layout_block_flow_->GetNode()->appendChild(insert); + + auto lines = MarkLineBoxesDirty(); + EXPECT_FALSE(lines[0]->IsDirty()); + EXPECT_TRUE(lines[1]->IsDirty()); +} + +// Test marking line boxes when appending a span on 2nd line. +TEST_P(NodeInsertTest, MarkLineBoxesDirtyOnAppendAfterBR) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; + SetupHtml("container", R"HTML( + <style> + .abspos { position: absolute; } + .float { float: left; } + </style> + <div id=container style="font-size: 10px; width: 10ch"> + <br> + <br> + </div> + )HTML"); + + Node* insert = (*GetParam())(GetDocument()); + layout_block_flow_->GetNode()->appendChild(insert); auto lines = MarkLineBoxesDirty(); EXPECT_FALSE(lines[0]->IsDirty()); @@ -1013,6 +1073,8 @@ TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnAppend2) { // Test marking line boxes when removing a span. TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnRemove) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; SetupHtml("container", R"HTML( <div id=container style="font-size: 10px; width: 10ch"> 1234<span id=t>5678</span> @@ -1028,6 +1090,8 @@ TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnRemove) { // Test marking line boxes when removing a span. TEST_P(NodeParameterTest, MarkLineBoxesDirtyOnRemoveFirst) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; SetupHtml("container", String(R"HTML( <div id=container style="font-size: 10px; width: 10ch">)HTML") + GetParam() + R"HTML(<span>after</span> @@ -1045,6 +1109,8 @@ TEST_P(NodeParameterTest, MarkLineBoxesDirtyOnRemoveFirst) { // Test marking line boxes when removing a span on 2nd line. TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnRemove2) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; SetupHtml("container", R"HTML( <div id=container style="font-size: 10px; width: 10ch"> 12345678 @@ -1062,6 +1128,8 @@ TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnRemove2) { // Test marking line boxes when removing a text node on 2nd line. TEST_P(NodeParameterTest, MarkLineBoxesDirtyOnRemoveAfterBR) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; SetupHtml("container", String(R"HTML( <div id=container style="font-size: 10px; width: 10ch"> line 1 @@ -1081,9 +1149,45 @@ TEST_P(NodeParameterTest, MarkLineBoxesDirtyOnRemoveAfterBR) { ForceLayout(); // Ensure running layout does not crash. } +TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnEndSpaceCollapsed) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; + SetupHtml("container", R"HTML( + <style> + div { + font-size: 10px; + width: 8ch; + } + #empty { + background: yellow; /* ensure fragment is created */ + } + #target { + display: inline-block; + } + </style> + <div id=container> + 1234567890 + 1234567890 + <span id=empty> </span> + <span id=target></span></div> + )HTML"); + + // Removing #target makes the spaces before it to be collapsed. + Element* target = GetElementById("target"); + target->remove(); + + auto lines = MarkLineBoxesDirty(); + EXPECT_FALSE(lines[0]->IsDirty()); + EXPECT_TRUE(lines[1]->IsDirty()); + + ForceLayout(); // Ensure running layout does not crash. +} + // Test marking line boxes when the first span has NeedsLayout. The span is // culled. TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnNeedsLayoutFirst) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; SetupHtml("container", R"HTML( <div id=container style="font-size: 10px; width: 10ch"> <span id=t>1234</span>5678 @@ -1100,6 +1204,8 @@ TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnNeedsLayoutFirst) { // Test marking line boxes when the first span has NeedsLayout. The span has a // box fragment. TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnNeedsLayoutFirstWithBox) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; SetupHtml("container", R"HTML( <div id=container style="font-size: 10px; width: 10ch"> <span id=t style="background: blue">1234</span>5678 @@ -1115,6 +1221,8 @@ TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnNeedsLayoutFirstWithBox) { // Test marking line boxes when a span has NeedsLayout. The span is culled. TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnNeedsLayout) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; SetupHtml("container", R"HTML( <div id=container style="font-size: 10px; width: 10ch"> 12345678 @@ -1133,6 +1241,8 @@ TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnNeedsLayout) { // Test marking line boxes when a span has NeedsLayout. The span has a box // fragment. TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnNeedsLayoutWithBox) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; SetupHtml("container", R"HTML( <div id=container style="font-size: 10px; width: 10ch"> 12345678 @@ -1152,6 +1262,8 @@ TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnNeedsLayoutWithBox) { // The parent span has a box fragment, and wraps, so that its fragment // is seen earlier in pre-order DFS. TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnChildOfWrappedBox) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; SetupHtml("container", R"HTML( <div id=container style="font-size: 10px"> <span style="background: yellow"> @@ -1172,6 +1284,8 @@ TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyOnChildOfWrappedBox) { // Test marking line boxes when a span has NeedsLayout. The span has a box // fragment. TEST_F(NGInlineNodeTest, MarkLineBoxesDirtyInInlineBlock) { + if (!RuntimeEnabledFeatures::LayoutNGLineCacheEnabled()) + return; SetupHtml("container", R"HTML( <div id=container style="display: inline-block; font-size: 10px"> 12345678<br> @@ -1226,6 +1340,71 @@ TEST_F(NGInlineNodeTest, RemoveInlineNodeDataIfBlockObtainsBlockChild) { EXPECT_FALSE(layout_block_flow_->HasNGInlineNodeData()); } +// Test inline objects are initialized when |SplitFlow()| moves them. +TEST_F(NGInlineNodeTest, ClearFirstInlineFragmentOnSplitFlow) { + SetBodyInnerHTML(R"HTML( + <div> + <span id=outer_span> + <span id=inner_span>1234</span> + </span> + </div> + )HTML"); + + // Keep the text fragment to compare later. + Element* inner_span = GetElementById("inner_span"); + Node* text = inner_span->firstChild(); + scoped_refptr<NGPaintFragment> text_fragment_before_split = + text->GetLayoutObject()->FirstInlineFragment(); + EXPECT_NE(text_fragment_before_split.get(), nullptr); + + // Append <div> to <span>. causing SplitFlow(). + Element* outer_span = GetElementById("outer_span"); + Element* div = GetDocument().CreateRawElement(html_names::kDivTag); + outer_span->appendChild(div); + + // Update tree but do NOT update layout. At this point, there's no guarantee, + // but there are some clients (e.g., Schroll Anchor) who try to read + // associated fragments. + // + // NGPaintFragment is owned by LayoutNGBlockFlow. Because the original owner + // no longer has an inline formatting context, the NGPaintFragment subtree is + // destroyed, and should not be accessible. + GetDocument().UpdateStyleAndLayoutTree(); + scoped_refptr<NGPaintFragment> text_fragment_before_layout = + text->GetLayoutObject()->FirstInlineFragment(); + EXPECT_EQ(text_fragment_before_layout, nullptr); + + // Update layout. There should be a different instance of the text fragment. + UpdateAllLifecyclePhasesForTest(); + scoped_refptr<NGPaintFragment> text_fragment_after_layout = + text->GetLayoutObject()->FirstInlineFragment(); + EXPECT_NE(text_fragment_before_split, text_fragment_after_layout); + + // Check it is the one owned by the new root inline formatting context. + LayoutBlock* anonymous_block = + inner_span->GetLayoutObject()->ContainingBlock(); + EXPECT_TRUE(anonymous_block->IsAnonymous()); + const NGPaintFragment* block_fragment = anonymous_block->PaintFragment(); + const NGPaintFragment* line_box_fragment = block_fragment->FirstChild(); + EXPECT_EQ(line_box_fragment->FirstChild(), text_fragment_after_layout); +} + +TEST_F(NGInlineNodeTest, AddChildToSVGRoot) { + SetBodyInnerHTML(R"HTML( + <div id="container"> + text + <svg id="svg"></svg> + </div> + )HTML"); + + Element* svg = GetElementById("svg"); + svg->appendChild(GetDocument().CreateRawElement(svg_names::kTextTag)); + GetDocument().UpdateStyleAndLayoutTree(); + + LayoutObject* container = GetLayoutObjectByElementId("container"); + EXPECT_FALSE(container->NeedsCollectInlines()); +} + // https://crbug.com/911220 TEST_F(NGInlineNodeTest, PreservedNewlineWithBidiAndRelayout) { SetupHtml("container", @@ -1241,6 +1420,44 @@ TEST_F(NGInlineNodeTest, PreservedNewlineWithBidiAndRelayout) { EXPECT_EQ(String(u"foo\u2066\u2069\n\u2066\u2069bar\nbaz"), GetText()); } +TEST_F(NGInlineNodeTest, PreservedNewlineWithRemovedBidiAndRelayout) { + SetupHtml("container", + "<pre id=container>foo<span dir=rtl>\nbar</span></pre>"); + EXPECT_EQ(String(u"foo\u2067\u2069\n\u2067bar\u2069"), GetText()); + + GetDocument().QuerySelector("span")->removeAttribute(html_names::kDirAttr); + UpdateAllLifecyclePhasesForTest(); + + // The bidi control characters around '\n' should not preserve + EXPECT_EQ("foo\nbar", GetText()); +} + +TEST_F(NGInlineNodeTest, PreservedNewlineWithRemovedLtrDirAndRelayout) { + SetupHtml("container", + "<pre id=container>foo<span dir=ltr>\nbar</span></pre>"); + EXPECT_EQ(String(u"foo\u2066\u2069\n\u2066bar\u2069"), GetText()); + + GetDocument().QuerySelector("span")->removeAttribute(html_names::kDirAttr); + UpdateAllLifecyclePhasesForTest(); + + // The bidi control characters around '\n' should not preserve + EXPECT_EQ("foo\nbar", GetText()); +} + +// https://crbug.com/969089 +TEST_F(NGInlineNodeTest, InsertedWBRWithLineBreakInRelayout) { + SetupHtml("container", "<div id=container><span>foo</span>\nbar</div>"); + EXPECT_EQ("foo bar", GetText()); + + Element* div = GetElementById("container"); + Element* wbr = GetDocument().CreateElementForBinding("wbr"); + div->insertBefore(wbr, div->lastChild()); + UpdateAllLifecyclePhasesForTest(); + + // The '\n' should be collapsed by the inserted <wbr> + EXPECT_EQ(String(u"foo\u200Bbar"), GetText()); +} + #if SEGMENT_BREAK_TRANSFORMATION_FOR_EAST_ASIAN_WIDTH // https://crbug.com/879088 TEST_F(NGInlineNodeTest, RemoveSegmentBreakFromJapaneseInRelayout) { 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 8d66c3cc915..13d2b5d6e37 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 @@ -16,7 +16,6 @@ namespace blink { void NGLineBoxFragmentBuilder::Reset() { children_.Shrink(0); - offsets_.Shrink(0); child_break_tokens_.Shrink(0); inline_break_tokens_.Shrink(0); oof_positioned_candidates_.Shrink(0); @@ -28,7 +27,7 @@ void NGLineBoxFragmentBuilder::Reset() { has_last_resort_break_ = false; has_floating_descendants_ = false; has_orthogonal_flow_roots_ = false; - has_child_that_depends_on_percentage_block_size_ = false; + has_descendant_that_depends_on_percentage_block_size_ = false; has_block_fragmentation_ = false; may_have_descendant_above_block_start_ = false; } @@ -57,6 +56,12 @@ NGLineBoxFragmentBuilder::ChildList::LastInFlowChild() { } void NGLineBoxFragmentBuilder::ChildList::MoveInInlineDirection( + LayoutUnit delta) { + for (auto& child : children_) + child.offset.inline_offset += delta; +} + +void NGLineBoxFragmentBuilder::ChildList::MoveInInlineDirection( LayoutUnit delta, unsigned start, unsigned end) { @@ -78,13 +83,12 @@ void NGLineBoxFragmentBuilder::ChildList::MoveInBlockDirection(LayoutUnit delta, } void NGLineBoxFragmentBuilder::AddChildren(ChildList& children) { - offsets_.ReserveCapacity(children.size()); children_.ReserveCapacity(children.size()); for (auto& child : children) { if (child.layout_result) { DCHECK(!child.fragment); - AddChild(*child.layout_result, child.offset); + AddChild(child.layout_result->PhysicalFragment(), child.offset); child.layout_result.reset(); } else if (child.fragment) { AddChild(std::move(child.fragment), child.offset); 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 573308b00b2..71c7396ed98 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 @@ -5,13 +5,15 @@ #ifndef NGLineBoxFragmentBuilder_h #define NGLineBoxFragmentBuilder_h -#include "third_party/blink/renderer/core/layout/ng/geometry/ng_logical_offset.h" +#include "third_party/blink/renderer/core/layout/geometry/logical_offset.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.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/layout/ng/ng_container_fragment_builder.h" #include "third_party/blink/renderer/core/layout/ng/ng_layout_result.h" +#include "third_party/blink/renderer/core/layout/ng/ng_physical_container_fragment.h" #include "third_party/blink/renderer/core/layout/ng/ng_positioned_float.h" #include "third_party/blink/renderer/platform/wtf/allocator.h" @@ -19,11 +21,10 @@ namespace blink { class ComputedStyle; class NGInlineBreakToken; -class NGPhysicalFragment; class CORE_EXPORT NGLineBoxFragmentBuilder final : public NGContainerFragmentBuilder { - STACK_ALLOCATED(); + DISALLOW_NEW(); public: NGLineBoxFragmentBuilder(NGInlineNode node, @@ -45,6 +46,14 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final return metrics_.LineHeight().ClampNegativeToZero(); } + void SetInlineSize(LayoutUnit inline_size) { + size_.inline_size = inline_size; + } + + void SetHangInlineSize(LayoutUnit hang_inline_size) { + hang_inline_size_ = hang_inline_size; + } + // Mark this line box is an "empty" line box. See NGLineBoxType. void SetIsEmptyLineBox(); @@ -67,12 +76,12 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final DISALLOW_NEW(); scoped_refptr<const NGLayoutResult> layout_result; - scoped_refptr<const NGPhysicalFragment> fragment; + scoped_refptr<const NGPhysicalTextFragment> fragment; LayoutObject* out_of_flow_positioned_box = nullptr; LayoutObject* unpositioned_float = nullptr; // The offset of the border box, initially in this child coordinate system. // |ComputeInlinePositions()| converts it to the offset within the line box. - NGLogicalOffset offset; + LogicalOffset offset; // The offset of a positioned float wrt. the root BFC. This should only be // set for positioned floats. NGBfcOffset bfc_offset; @@ -90,29 +99,29 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final Child() = default; // Create a placeholder. A placeholder does not have a fragment nor a bidi // level. - Child(NGLogicalOffset offset) : offset(offset) {} + Child(LogicalOffset offset) : offset(offset) {} // Crete a bidi control. A bidi control does not have a fragment, but has // bidi level and affects bidi reordering. Child(UBiDiLevel bidi_level) : bidi_level(bidi_level) {} // Create an in-flow |NGLayoutResult|. Child(scoped_refptr<const NGLayoutResult> layout_result, - NGLogicalOffset offset, + LogicalOffset offset, LayoutUnit inline_size, UBiDiLevel bidi_level) : layout_result(std::move(layout_result)), offset(offset), inline_size(inline_size), bidi_level(bidi_level) {} - // Create an in-flow |NGPhysicalFragment|. - Child(scoped_refptr<const NGPhysicalFragment> fragment, - NGLogicalOffset offset, + // Create an in-flow |NGPhysicalTextFragment|. + Child(scoped_refptr<const NGPhysicalTextFragment> fragment, + LogicalOffset offset, LayoutUnit inline_size, UBiDiLevel bidi_level) : fragment(std::move(fragment)), offset(offset), inline_size(inline_size), bidi_level(bidi_level) {} - Child(scoped_refptr<const NGPhysicalFragment> fragment, + Child(scoped_refptr<const NGPhysicalTextFragment> fragment, LayoutUnit block_offset, LayoutUnit inline_size, UBiDiLevel bidi_level) @@ -142,7 +151,7 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final if (fragment) return true; - if (layout_result && !layout_result->PhysicalFragment()->IsFloating()) + if (layout_result && !layout_result->PhysicalFragment().IsFloating()) return true; return false; @@ -154,7 +163,9 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final bool HasBidiLevel() const { return bidi_level != 0xff; } bool IsPlaceholder() const { return !HasFragment() && !HasBidiLevel(); } const NGPhysicalFragment* PhysicalFragment() const { - return layout_result ? layout_result->PhysicalFragment() : fragment.get(); + if (layout_result) + return &layout_result->PhysicalFragment(); + return fragment.get(); } }; @@ -162,7 +173,7 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final // Unlike the fragment builder, chlidren are mutable. // Callers can add to the fragment builder in a batch once finalized. class ChildList { - STACK_ALLOCATED(); + DISALLOW_NEW(); public: ChildList() = default; @@ -204,13 +215,14 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final } void InsertChild(unsigned index, scoped_refptr<const NGLayoutResult> layout_result, - const NGLogicalOffset& offset, + const LogicalOffset& offset, LayoutUnit inline_size, UBiDiLevel bidi_level) { children_.insert(index, Child{std::move(layout_result), offset, inline_size, bidi_level}); } + void MoveInInlineDirection(LayoutUnit); void MoveInInlineDirection(LayoutUnit, unsigned start, unsigned end); void MoveInBlockDirection(LayoutUnit); void MoveInBlockDirection(LayoutUnit, unsigned start, unsigned end); @@ -227,6 +239,7 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final private: NGLineHeightMetrics metrics_; + LayoutUnit hang_inline_size_; NGPhysicalLineBoxFragment::NGLineBoxType line_box_type_; TextDirection base_direction_; 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 d36e73f5419..8f60a66ffb1 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 @@ -157,6 +157,14 @@ inline void ClearNeedsLayout(const NGInlineItem& item) { } // namespace +LayoutUnit NGLineBreaker::ComputeAvailableWidth() const { + LayoutUnit available_width = line_opportunity_.AvailableInlineSize(); + // Available width must be smaller than |LayoutUnit::Max()| so that the + // position can be larger. + available_width = std::min(available_width, LayoutUnit::NearlyMax()); + return available_width; +} + NGLineBreaker::NGLineBreaker(NGInlineNode node, NGLineBreakerMode mode, const NGConstraintSpace& space, @@ -184,6 +192,7 @@ NGLineBreaker::NGLineBreaker(NGInlineNode node, leading_floats_(leading_floats), handled_leading_floats_index_(handled_leading_floats_index), base_direction_(node_.BaseDirection()) { + available_width_ = ComputeAvailableWidth(); break_iterator_.SetBreakSpace(BreakSpaceType::kBeforeSpaceRun); if (break_token) { @@ -235,20 +244,23 @@ void NGLineBreaker::SetMaxSizeCache(MaxSizeCache* max_size_cache) { max_size_cache_ = max_size_cache; } -void NGLineBreaker::SetLineEndFragment( +LayoutUnit NGLineBreaker::SetLineEndFragment( scoped_refptr<const NGPhysicalTextFragment> fragment, NGLineInfo* line_info) { + LayoutUnit inline_size; bool is_horizontal = IsHorizontalWritingMode(constraint_space_.GetWritingMode()); if (line_info->LineEndFragment()) { - const NGPhysicalSize& size = line_info->LineEndFragment()->Size(); - position_ -= is_horizontal ? size.width : size.height; + const PhysicalSize& size = line_info->LineEndFragment()->Size(); + inline_size = is_horizontal ? -size.width : -size.height; } if (fragment) { - const NGPhysicalSize& size = fragment->Size(); - position_ += is_horizontal ? size.width : size.height; + const PhysicalSize& size = fragment->Size(); + inline_size = is_horizontal ? size.width : size.height; } line_info->SetLineEndFragment(std::move(fragment)); + position_ += inline_size; + return inline_size; } // Compute the base direction for bidi algorithm for this line. @@ -273,8 +285,8 @@ void NGLineBreaker::ComputeBaseDirection() { void NGLineBreaker::PrepareNextLine(NGLineInfo* line_info) { // NGLineInfo is not supposed to be re-used becase it's not much gain and to // avoid rare code path. - NGInlineItemResults* item_results = line_info->MutableResults(); - DCHECK(item_results->IsEmpty()); + const NGInlineItemResults& item_results = line_info->Results(); + DCHECK(item_results.IsEmpty()); if (item_index_) { // We're past the first line @@ -310,6 +322,8 @@ void NGLineBreaker::PrepareNextLine(NGLineInfo* line_info) { // Use 'text-indent' as the initial position. This lets tab positions to align // regardless of 'text-indent'. position_ = line_info->TextIndent(); + + overflow_item_index_ = 0; } void NGLineBreaker::NextLine( @@ -326,9 +340,9 @@ void NGLineBreaker::NextLine( out_floats_for_min_max, line_info); RemoveTrailingCollapsibleSpace(line_info); - NGInlineItemResults* item_results = line_info->MutableResults(); + const NGInlineItemResults& item_results = line_info->Results(); #if DCHECK_IS_ON() - for (const auto& result : *item_results) + for (const auto& result : item_results) result.CheckConsistency(mode_ == NGLineBreakerMode::kMinContent); #endif @@ -340,7 +354,7 @@ void NGLineBreaker::NextLine( // // TODO(kojii): There are cases where we need to PlaceItems() without creating // line boxes. These cases need to be reviewed. - bool should_create_line_box = ShouldCreateLineBox(*item_results) || + bool should_create_line_box = ShouldCreateLineBox(item_results) || (has_list_marker_ && line_info->IsLastLine()) || mode_ != NGLineBreakerMode::kContent; @@ -377,7 +391,7 @@ void NGLineBreaker::BreakLine( return; } - NGInlineItemResults* item_results = line_info->MutableResults(); + const NGInlineItemResults& item_results = line_info->Results(); // Handle trailable items first. These items may not be break before. // They (or part of them) may also overhang the available width. @@ -388,11 +402,16 @@ void NGLineBreaker::BreakLine( else HandleEmptyText(item, line_info); #if DCHECK_IS_ON() - if (!item_results->IsEmpty()) - item_results->back().CheckConsistency(true); + if (!item_results.IsEmpty()) + item_results.back().CheckConsistency(true); #endif continue; } + if (item.Type() == NGInlineItem::kOpenTag) { + if (HandleOpenTag(item, line_info)) + continue; + return; + } if (item.Type() == NGInlineItem::kCloseTag) { HandleCloseTag(item, line_info); continue; @@ -413,12 +432,12 @@ void NGLineBreaker::BreakLine( // Items after this point are not trailable. Break at the earliest break // opportunity if we're trailing. if (state_ == LineBreakState::kTrailing && - CanBreakAfterLast(*item_results)) { + CanBreakAfterLast(item_results)) { // If the sticky images quirk is enabled, and this is an image that // follows text that doesn't end with something breakable, we cannot break // between the two items. if (sticky_images_quirk_ && - IsStickyImage(item, *item_results, trailing_whitespace_, Text())) { + IsStickyImage(item, item_results, trailing_whitespace_, Text())) { HandleAtomicInline(item, percentage_resolution_block_size_for_min_max, line_info); continue; @@ -431,8 +450,6 @@ void NGLineBreaker::BreakLine( if (item.Type() == NGInlineItem::kAtomicInline) { HandleAtomicInline(item, percentage_resolution_block_size_for_min_max, line_info); - } else if (item.Type() == NGInlineItem::kOpenTag) { - HandleOpenTag(item, line_info); } else if (item.Type() == NGInlineItem::kOutOfFlowPositioned) { AddItem(item, line_info); MoveToNextOf(item); @@ -465,6 +482,8 @@ void NGLineBreaker::ComputeLineLocation(NGLineInfo* line_info) const { line_info->SetWidth(available_width, position_); line_info->SetBfcOffset( {line_opportunity_.line_left_offset, line_opportunity_.bfc_block_offset}); + if (mode_ == NGLineBreakerMode::kContent) + line_info->UpdateTextAlign(); } void NGLineBreaker::HandleText(const NGInlineItem& item, @@ -477,7 +496,7 @@ void NGLineBreaker::HandleText(const NGInlineItem& item, // If we're trailing, only trailing spaces can be included in this line. if (state_ == LineBreakState::kTrailing) { - if (CanBreakAfterLast(*line_info->MutableResults())) + if (CanBreakAfterLast(line_info->Results())) return HandleTrailingSpaces(item, shape_result, line_info); // When a run of preserved spaces are across items, |CanBreakAfterLast| is // false for between spaces. But we still need to handle them as trailing @@ -519,17 +538,18 @@ void NGLineBreaker::HandleText(const NGInlineItem& item, // Try to break inside of this text item. LayoutUnit available_width = AvailableWidthToFit(); - BreakText(item_result, item, shape_result, available_width - position_, - line_info); - - LayoutUnit next_position = position_ + item_result->inline_size; - bool is_overflow = next_position > available_width; - DCHECK(is_overflow || item_result->shape_result); - position_ = next_position; - item_result->may_break_inside = !is_overflow; + BreakResult break_result = + BreakText(item_result, item, shape_result, available_width - position_, + line_info); + DCHECK(item_result->shape_result || + (break_result == kOverflow && break_anywhere_if_overflow_ && + !override_break_anywhere_)); + position_ += item_result->inline_size; + DCHECK_EQ(break_result == kSuccess, position_ <= available_width); + item_result->may_break_inside = break_result == kSuccess; MoveToNextOf(*item_result); - if (!is_overflow || + if (break_result == kSuccess || (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 @@ -542,15 +562,27 @@ void NGLineBreaker::HandleText(const NGInlineItem& item, return; } + DCHECK_EQ(break_result, kOverflow); return HandleOverflow(line_info); } - // Add the whole item if !auto_wrap. The previous line should not have wrapped - // in the middle of nowrap item. - DCHECK_EQ(item_result->start_offset, item.StartOffset()); + // Add until the end of the item if !auto_wrap. In most cases, it's the whole + // item. DCHECK_EQ(item_result->end_offset, item.EndOffset()); - item_result->inline_size = shape_result.SnappedWidth().ClampNegativeToZero(); - item_result->shape_result = ShapeResultView::Create(&shape_result); + if (item_result->start_offset == item.StartOffset()) { + item_result->inline_size = + shape_result.SnappedWidth().ClampNegativeToZero(); + item_result->shape_result = ShapeResultView::Create(&shape_result); + } else { + // <wbr> can wrap even if !auto_wrap. Spaces after that will be leading + // spaces and thus be collapsed. + DCHECK(trailing_whitespace_ == WhitespaceState::kLeading && + item_result->start_offset >= item.StartOffset()); + item_result->shape_result = ShapeResultView::Create( + &shape_result, item_result->start_offset, item_result->end_offset); + item_result->inline_size = + item_result->shape_result->SnappedWidth().ClampNegativeToZero(); + } DCHECK(!item_result->may_break_inside); DCHECK(!item_result->can_break_after); @@ -559,21 +591,18 @@ void NGLineBreaker::HandleText(const NGInlineItem& item, MoveToNextOf(item); } -void NGLineBreaker::BreakText(NGInlineItemResult* item_result, - const NGInlineItem& item, - const ShapeResult& item_shape_result, - LayoutUnit available_width, - NGLineInfo* line_info) { +NGLineBreaker::BreakResult NGLineBreaker::BreakText( + NGInlineItemResult* item_result, + const NGInlineItem& item, + const ShapeResult& item_shape_result, + LayoutUnit available_width, + NGLineInfo* line_info) { DCHECK(item.Type() == NGInlineItem::kText || (item.Type() == NGInlineItem::kControl && Text()[item.StartOffset()] == kTabulationCharacter)); DCHECK(&item_shape_result); item.AssertOffset(item_result->start_offset); - // TODO(kojii): We need to instantiate ShapingLineBreaker here because it - // has item-specific info as context. Should they be part of ShapeLine() to - // instantiate once, or is this just fine since instatiation is not - // expensive? DCHECK_EQ(item_shape_result.StartIndex(), item.StartOffset()); DCHECK_EQ(item_shape_result.EndIndex(), item.EndOffset()); struct ShapeCallbackContext { @@ -593,7 +622,6 @@ void NGLineBreaker::BreakText(NGInlineItemResult* item_result, shape_callback, &shape_callback_context); 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; @@ -612,36 +640,58 @@ void NGLineBreaker::BreakText(NGInlineItemResult* item_result, if (break_anywhere_if_overflow_ && !override_break_anywhere_) options |= ShapingLineBreaker::kNoResultIfOverflow; - ShapingLineBreaker::Result result; - scoped_refptr<const ShapeResultView> 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) { - SetLineEndFragment( - CreateHyphenFragment(node_, constraint_space_.GetWritingMode(), item), - line_info); - - // TODO(kojii): Implement when adding a hyphen caused overflow. - // crbug.com/714962: Should be removed when switched to NGPaint. - item_result->text_end_effect = NGTextEndEffect::kHyphen; +#if DCHECK_IS_ON() + unsigned try_count = 0; +#endif + LayoutUnit inline_size; + while (true) { +#if DCHECK_IS_ON() + ++try_count; + DCHECK_LE(try_count, 2u); +#endif + ShapingLineBreaker::Result result; + scoped_refptr<const ShapeResultView> shape_result = breaker.ShapeLine( + item_result->start_offset, available_width.ClampNegativeToZero(), + 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 kOverflow; + } + DCHECK_EQ(shape_result->NumCharacters(), + result.break_offset - item_result->start_offset); + // It is critical to move the offset forward, or NGLineBreaker may keep + // adding NGInlineItemResult until all the memory is consumed. + CHECK_GT(result.break_offset, item_result->start_offset); + + inline_size = shape_result->SnappedWidth().ClampNegativeToZero(); + item_result->inline_size = inline_size; + if (UNLIKELY(result.is_hyphenated)) { + const WritingMode writing_mode = constraint_space_.GetWritingMode(); + scoped_refptr<const NGPhysicalTextFragment> hyphen_fragment = + CreateHyphenFragment(node_, writing_mode, item); + LayoutUnit space_for_hyphen = available_width - inline_size; + LayoutUnit hyphen_inline_size = IsHorizontalWritingMode(writing_mode) + ? hyphen_fragment->Size().width + : hyphen_fragment->Size().height; + // If the hyphen overflows, retry with the reduced available width. + if (space_for_hyphen >= 0 && hyphen_inline_size > space_for_hyphen) { + available_width -= hyphen_inline_size; + continue; + } + inline_size += SetLineEndFragment(std::move(hyphen_fragment), line_info); + item_result->text_end_effect = NGTextEndEffect::kHyphen; + } + item_result->inline_size = + shape_result->SnappedWidth().ClampNegativeToZero(); + item_result->end_offset = result.break_offset; + item_result->shape_result = std::move(shape_result); + break; } - item_result->inline_size = shape_result->SnappedWidth().ClampNegativeToZero(); - item_result->end_offset = result.break_offset; - item_result->shape_result = std::move(shape_result); - // It is critical to move offset forward, or NGLineBreaker may keep adding - // NGInlineItemResult until all the memory is consumed. - CHECK_GT(item_result->end_offset, item_result->start_offset) << Text(); // * If width <= available_width: // * If offset < item.EndOffset(): the break opportunity to fit is found. @@ -666,6 +716,7 @@ void NGLineBreaker::BreakText(NGInlineItemResult* item_result, break_iterator_.IsBreakable(item_result->end_offset); trailing_whitespace_ = WhitespaceState::kUnknown; } + return inline_size <= available_width ? kSuccess : kOverflow; } // This function handles text item for min-content. The specialized logic is @@ -1121,10 +1172,9 @@ void NGLineBreaker::HandleAtomicInline( .LayoutAtomicInline(constraint_space_, node_.Style(), line_info->LineStyle().GetFontBaseline(), line_info->UseFirstLineStyle()); - DCHECK(item_result->layout_result->PhysicalFragment()); item_result->inline_size = NGFragment(constraint_space_.GetWritingMode(), - *item_result->layout_result->PhysicalFragment()) + item_result->layout_result->PhysicalFragment()) .InlineSize(); item_result->margins = ComputeLineMarginsForVisualContainer(constraint_space_, style); @@ -1199,7 +1249,7 @@ void NGLineBreaker::HandleFloat(const NGInlineItem& item, // fragmentainer break. In that case the floats associated with this line will // already have been processed. NGInlineItemResult* item_result = AddItem(item, line_info); - ComputeCanBreakAfter(item_result, auto_wrap_, break_iterator_); + item_result->can_break_after = auto_wrap_; MoveToNextOf(item); // If we are currently computing our min/max-content size simply append to @@ -1261,7 +1311,7 @@ void NGLineBreaker::HandleFloat(const NGInlineItem& item, // Check if we already have a pending float. That's because a float cannot be // higher than any block or floated box generated before. - if (HasUnpositionedFloats(*line_info->MutableResults()) || float_after_line) { + if (HasUnpositionedFloats(line_info->Results()) || float_after_line) { item_result->has_unpositioned_floats = true; } else { NGPositionedFloat positioned_float = PositionFloat( @@ -1276,12 +1326,13 @@ void NGLineBreaker::HandleFloat(const NGInlineItem& item, NGLayoutOpportunity opportunity = exclusion_space_->FindLayoutOpportunity( {constraint_space_.BfcOffset().line_offset, bfc_block_offset}, - constraint_space_.AvailableSize().inline_size, NGLogicalSize()); + constraint_space_.AvailableSize().inline_size, LogicalSize()); DCHECK_EQ(bfc_block_offset, opportunity.rect.BlockStartOffset()); line_opportunity_ = opportunity.ComputeLineLayoutOpportunity( constraint_space_, line_opportunity_.line_block_size, LayoutUnit()); + available_width_ = ComputeAvailableWidth(); DCHECK_GE(AvailableWidth(), LayoutUnit()); } @@ -1290,7 +1341,8 @@ void NGLineBreaker::HandleFloat(const NGInlineItem& item, bool NGLineBreaker::ComputeOpenTagResult( const NGInlineItem& item, const NGConstraintSpace& constraint_space, - NGInlineItemResult* item_result) { + NGInlineItemResult* item_result, + base::Optional<NGLineBoxStrut> margins) { DCHECK_EQ(item.Type(), NGInlineItem::kOpenTag); DCHECK(item.Style()); const ComputedStyle& style = *item.Style(); @@ -1301,7 +1353,9 @@ bool NGLineBreaker::ComputeOpenTagResult( item_result->borders = ComputeLineBorders(style); item_result->padding = ComputeLinePadding(constraint_space, style); if (item_result->has_edge) { - item_result->margins = ComputeLineMarginsForSelf(constraint_space, style); + item_result->margins = + margins ? *margins + : ComputeLineMarginsForSelf(constraint_space, style); item_result->inline_size = item_result->margins.inline_start + item_result->borders.inline_start + item_result->padding.inline_start; @@ -1311,11 +1365,39 @@ bool NGLineBreaker::ComputeOpenTagResult( return false; } -void NGLineBreaker::HandleOpenTag(const NGInlineItem& item, +bool NGLineBreaker::HandleOpenTag(const NGInlineItem& item, NGLineInfo* line_info) { + DCHECK_EQ(item.Type(), NGInlineItem::kOpenTag); + DCHECK(item.Style()); + const ComputedStyle& style = *item.Style(); + + // OpenTag is not trailable, except when it has negative inline-start margin, + // which can bring the position back to inside of the available width. + base::Optional<NGLineBoxStrut> margins; + if (UNLIKELY(state_ == LineBreakState::kTrailing && + CanBreakAfterLast(line_info->Results()))) { + bool can_continue = false; + if (UNLIKELY(item_index_ >= overflow_item_index_ && + item.ShouldCreateBoxFragment() && item.HasStartEdge() && + style.MayHaveMargin())) { + margins = ComputeLineMarginsForSelf(constraint_space_, style); + LayoutUnit inline_start_margin = margins->inline_start; + can_continue = inline_start_margin < 0 && + position_ + inline_start_margin < AvailableWidthToFit(); + } + if (!can_continue) { + // Not that case. Break the line before this OpenTag. + line_info->SetIsLastLine(false); + return false; + } + // The state is back to normal because the position is back to inside of the + // available width. + state_ = LineBreakState::kContinue; + } + NGInlineItemResult* item_result = AddItem(item, line_info); - if (ComputeOpenTagResult(item, constraint_space_, item_result)) { + if (ComputeOpenTagResult(item, constraint_space_, item_result, margins)) { position_ += item_result->inline_size; // While the spec defines "non-zero margins, padding, or borders" prevents @@ -1327,16 +1409,15 @@ void NGLineBreaker::HandleOpenTag(const NGInlineItem& item, } bool was_auto_wrap = auto_wrap_; - DCHECK(item.Style()); - const ComputedStyle& style = *item.Style(); SetCurrentStyle(style); MoveToNextOf(item); DCHECK(!item_result->can_break_after); - NGInlineItemResults* item_results = line_info->MutableResults(); - if (UNLIKELY(!was_auto_wrap && auto_wrap_ && item_results->size() >= 2)) { + const NGInlineItemResults& item_results = line_info->Results(); + if (UNLIKELY(!was_auto_wrap && auto_wrap_ && item_results.size() >= 2)) { ComputeCanBreakAfter(std::prev(item_result), auto_wrap_, break_iterator_); } + return true; } void NGLineBreaker::HandleCloseTag(const NGInlineItem& item, @@ -1360,8 +1441,8 @@ void NGLineBreaker::HandleCloseTag(const NGInlineItem& item, // If the line can break after the previous item, prohibit it and allow break // after this close tag instead. if (was_auto_wrap) { - NGInlineItemResults* item_results = line_info->MutableResults(); - if (item_results->size() >= 2) { + const NGInlineItemResults& item_results = line_info->Results(); + if (item_results.size() >= 2) { NGInlineItemResult* last = std::prev(item_result); item_result->can_break_after = last->can_break_after; last->can_break_after = false; @@ -1390,6 +1471,8 @@ void NGLineBreaker::HandleCloseTag(const NGInlineItem& item, // At this point, item_results does not fit into the current line, and there // are no break opportunities in item_results.back(). void NGLineBreaker::HandleOverflow(NGLineInfo* line_info) { + overflow_item_index_ = std::max(overflow_item_index_, item_index_); + // Compute the width needing to rewind. When |width_to_rewind| goes negative, // items can fit within the line. LayoutUnit available_width = AvailableWidthToFit(); @@ -1435,14 +1518,18 @@ void NGLineBreaker::HandleOverflow(NGLineInfo* line_info) { LayoutUnit item_available_width = std::min(-width_to_rewind, item_result->inline_size - 1); SetCurrentStyle(*item.Style()); - BreakText(item_result, item, *item.TextShapeResult(), - item_available_width, line_info); + BreakResult break_result = + BreakText(item_result, item, *item.TextShapeResult(), + item_available_width, line_info); #if DCHECK_IS_ON() 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_EQ(break_result == kSuccess, + item_result->inline_size <= item_available_width); + if (break_result == kSuccess) { + DCHECK_LE(item_result->inline_size, item_available_width); + DCHECK_LT(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()) { @@ -1478,6 +1565,7 @@ void NGLineBreaker::HandleOverflow(NGLineInfo* line_info) { if (!item_results->IsEmpty()) Rewind(0, line_info); state_ = LineBreakState::kContinue; + overflow_item_index_ = 0; return; } @@ -1508,6 +1596,8 @@ void NGLineBreaker::Rewind(unsigned new_end, NGLineInfo* line_info) { // most cases where our support for rewinding positioned floats is not great // yet (see below.) while (item_results[new_end].item->Type() == NGInlineItem::kFloating) { + // We assume floats can break after, or this may cause an infinite loop. + DCHECK(item_results[new_end].can_break_after); ++new_end; if (new_end == item_results.size()) { position_ = line_info->ComputeWidth(); @@ -1520,6 +1610,8 @@ void NGLineBreaker::Rewind(unsigned new_end, NGLineInfo* line_info) { for (unsigned i = item_results.size(); i > new_end;) { NGInlineItemResult& rewind = item_results[--i]; if (rewind.positioned_float) { + // We assume floats can break after, or this may cause an infinite loop. + DCHECK(rewind.can_break_after); // 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 @@ -1560,7 +1652,7 @@ void NGLineBreaker::Rewind(unsigned new_end, NGLineInfo* line_info) { const ComputedStyle& NGLineBreaker::ComputeCurrentStyle( unsigned item_result_index, NGLineInfo* line_info) const { - NGInlineItemResults& item_results = *line_info->MutableResults(); + const NGInlineItemResults& item_results = line_info->Results(); // Use the current item if it can compute the current style. const NGInlineItem* item = item_results[item_result_index].item; @@ -1616,8 +1708,10 @@ void NGLineBreaker::SetCurrentStyle(const ComputedStyle& style) { line_break_type = LineBreakType::kKeepAll; break; } - if (UNLIKELY(override_break_anywhere_ && break_anywhere_if_overflow_)) + if (UNLIKELY((override_break_anywhere_ && break_anywhere_if_overflow_) || + style.GetLineBreak() == LineBreak::kAnywhere)) { line_break_type = LineBreakType::kBreakCharacter; + } break_iterator_.SetBreakType(line_break_type); enable_soft_hyphen_ = style.GetHyphens() != Hyphens::kNone; 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 1e9baf8885b..17c3d0545a0 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 @@ -47,7 +47,7 @@ class CORE_EXPORT NGLineBreaker { // Compute the next line break point and produces NGInlineItemResults for // the line. inline void NextLine(NGLineInfo* line_info) { - NextLine(NGSizeIndefinite, nullptr, line_info); + NextLine(kIndefiniteSize, nullptr, line_info); } // During the min/max size calculation we need a special percentage @@ -72,9 +72,11 @@ class CORE_EXPORT NGLineBreaker { // Compute NGInlineItemResult for an open tag item. // Returns true if this item has edge and may have non-zero inline size. - static bool ComputeOpenTagResult(const NGInlineItem&, - const NGConstraintSpace&, - NGInlineItemResult*); + static bool ComputeOpenTagResult( + const NGInlineItem&, + const NGConstraintSpace&, + NGInlineItemResult*, + base::Optional<NGLineBoxStrut> margins = base::nullopt); // This enum is private, except for |WhitespaceStateForTesting()|. See // |whitespace_| member. @@ -98,8 +100,8 @@ class CORE_EXPORT NGLineBreaker { unsigned end_offset, NGLineInfo*); NGInlineItemResult* AddItem(const NGInlineItem&, NGLineInfo*); - void SetLineEndFragment(scoped_refptr<const NGPhysicalTextFragment>, - NGLineInfo*); + LayoutUnit SetLineEndFragment(scoped_refptr<const NGPhysicalTextFragment>, + NGLineInfo*); void BreakLine(LayoutUnit percentage_resolution_block_size_for_min_max, Vector<LayoutObject*>* out_floats_for_min_max, @@ -126,11 +128,12 @@ class CORE_EXPORT NGLineBreaker { HandleText(item, *item.TextShapeResult(), line_info); } void HandleText(const NGInlineItem& item, const ShapeResult&, NGLineInfo*); - void BreakText(NGInlineItemResult*, - const NGInlineItem&, - const ShapeResult&, - LayoutUnit available_width, - NGLineInfo*); + enum BreakResult { kSuccess, kOverflow }; + BreakResult BreakText(NGInlineItemResult*, + const NGInlineItem&, + const ShapeResult&, + LayoutUnit available_width, + NGLineInfo*); bool HandleTextForFastMinContent(NGInlineItemResult*, const NGInlineItem&, const ShapeResult&, @@ -163,7 +166,7 @@ class CORE_EXPORT NGLineBreaker { Vector<LayoutObject*>* out_floats_for_min_max, NGLineInfo*); - void HandleOpenTag(const NGInlineItem&, NGLineInfo*); + bool HandleOpenTag(const NGInlineItem&, NGLineInfo*); void HandleCloseTag(const NGInlineItem&, NGLineInfo*); void HandleOverflow(NGLineInfo*); @@ -179,11 +182,13 @@ class CORE_EXPORT NGLineBreaker { void ComputeBaseDirection(); LayoutUnit AvailableWidth() const { - return line_opportunity_.AvailableInlineSize(); + DCHECK_EQ(available_width_, ComputeAvailableWidth()); + return available_width_; } LayoutUnit AvailableWidthToFit() const { return AvailableWidth().AddEpsilon(); } + LayoutUnit ComputeAvailableWidth() const; // Represents the current offset of the input. LineBreakState state_; @@ -197,6 +202,7 @@ class CORE_EXPORT NGLineBreaker { // The current position from inline_start. Unlike NGInlineLayoutAlgorithm // that computes position in visual order, this position in logical order. LayoutUnit position_; + LayoutUnit available_width_; NGLineLayoutOpportunity line_opportunity_; NGInlineNode node_; @@ -261,6 +267,9 @@ class CORE_EXPORT NGLineBreaker { }; base::Optional<TrailingCollapsibleSpace> trailing_collapsible_space_; + // Keep track of item index where overflow occurrred. + unsigned overflow_item_index_; + // Keep track of handled float items. See HandleFloat(). const NGPositionedFloatVector& leading_floats_; unsigned leading_floats_index_ = 0u; 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 861fd965e43..778bb892eec 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 @@ -35,10 +35,10 @@ class NGLineBreakerTest : public NGLayoutTest { node.PrepareLayoutIfNeeded(); NGConstraintSpace space = - NGConstraintSpaceBuilder( - WritingMode::kHorizontalTb, WritingMode::kHorizontalTb, - /* is_new_fc */ false) - .SetAvailableSize({available_width, NGSizeIndefinite}) + NGConstraintSpaceBuilder(WritingMode::kHorizontalTb, + WritingMode::kHorizontalTb, + /* is_new_fc */ false) + .SetAvailableSize({available_width, kIndefiniteSize}) .ToConstraintSpace(); scoped_refptr<NGInlineBreakToken> break_token; @@ -494,8 +494,13 @@ TEST_P(NGTrailingSpaceWidthTest, TrailingSpaceWidth) { )HTML"); Vector<NGLineInfo> line_infos = BreakToLineInfo(node, LayoutUnit(50)); - EXPECT_EQ(line_infos[0].ComputeTrailingSpaceWidth(), - LayoutUnit(10) * data.trailing_space_width); + const NGLineInfo& line_info = line_infos[0]; + if (line_info.ShouldHangTrailingSpaces()) { + EXPECT_EQ(line_info.HangWidth(), + LayoutUnit(10) * data.trailing_space_width); + } else { + EXPECT_EQ(line_info.HangWidth(), LayoutUnit()); + } } TEST_F(NGLineBreakerTest, MinMaxWithTrailingSpaces) { 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 97a947e04a5..747f3f13a07 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 @@ -13,29 +13,6 @@ namespace blink { -namespace { - -// Create the style to use for the ellipsis characters. -// -// The ellipsis is styled according to the line style. -// https://drafts.csswg.org/css-ui/#ellipsing-details -scoped_refptr<const ComputedStyle> CreateEllipsisStyle( - scoped_refptr<const ComputedStyle> line_style) { - if (line_style->TextDecorationsInEffect() == TextDecoration::kNone) - return line_style; - - // Ellipsis should not have text decorations. Reset if it's set. - // This is not defined, but 4 impls do this. - scoped_refptr<ComputedStyle> ellipsis_style = - ComputedStyle::CreateAnonymousStyleWithDisplay(*line_style, - EDisplay::kInline); - ellipsis_style->ResetTextDecoration(); - ellipsis_style->ClearAppliedTextDecorations(); - return ellipsis_style; -} - -} // namespace - NGLineTruncator::NGLineTruncator(NGInlineNode& node, const NGLineInfo& line_info) : node_(node), @@ -47,8 +24,9 @@ LayoutUnit NGLineTruncator::TruncateLine( LayoutUnit line_width, NGLineBoxFragmentBuilder::ChildList* line_box) { // Shape the ellipsis and compute its inline size. - scoped_refptr<const ComputedStyle> ellipsis_style = - CreateEllipsisStyle(line_style_); + // The ellipsis is styled according to the line style. + // https://drafts.csswg.org/css-ui/#ellipsing-details + const ComputedStyle* ellipsis_style = line_style_.get(); const Font& font = ellipsis_style->GetFont(); const SimpleFontData* font_data = font.PrimaryFont(); DCHECK(font_data); @@ -101,7 +79,7 @@ LayoutUnit NGLineTruncator::TruncateLine( NGTextFragmentBuilder builder(node_, line_style_->GetWritingMode()); DCHECK(ellipsized_fragment->GetLayoutObject() && ellipsized_fragment->GetLayoutObject()->IsInline()); - builder.SetText(ellipsized_fragment->GetLayoutObject(), ellipsis_text, + builder.SetText(ellipsized_fragment->GetMutableLayoutObject(), ellipsis_text, ellipsis_style, true /* is_ellipsis_style */, std::move(ellipsis_shape_result)); FontBaseline baseline_type = line_style_->GetFontBaseline(); @@ -109,7 +87,7 @@ LayoutUnit NGLineTruncator::TruncateLine( baseline_type); line_box->AddChild( builder.ToTextFragment(), - NGLogicalOffset{ellipsis_inline_offset, -ellipsis_metrics.ascent}, + LogicalOffset{ellipsis_inline_offset, -ellipsis_metrics.ascent}, ellipsis_width, 0); return std::max(ellipsis_inline_offset + ellipsis_width, line_width); } @@ -121,10 +99,9 @@ void NGLineTruncator::HideChild(NGLineBoxFragmentBuilder::Child* child) { const NGPhysicalFragment* fragment = nullptr; if (const NGLayoutResult* layout_result = child->layout_result.get()) { // Need to propagate OOF descendants in this inline-block child. - if (!layout_result->OutOfFlowPositionedDescendants().IsEmpty()) { + if (layout_result->PhysicalFragment().HasOutOfFlowPositionedDescendants()) return; - } - fragment = layout_result->PhysicalFragment(); + fragment = &layout_result->PhysicalFragment(); } else { fragment = child->fragment.get(); } @@ -211,6 +188,11 @@ bool NGLineTruncator::TruncateChild(LayoutUnit space_for_child, if (!child->fragment) return is_first_child; auto& fragment = To<NGPhysicalTextFragment>(*child->fragment); + + // No need to truncate empty results. + if (!fragment.TextShapeResult()) + return is_first_child; + // TODO(layout-dev): Add support for OffsetToFit to ShapeResultView to avoid // this copy. scoped_refptr<blink::ShapeResult> shape_result = diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.cc index fab78c10ead..1e7fb8e2a92 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.cc @@ -12,7 +12,6 @@ #include "third_party/blink/renderer/core/editing/ephemeral_range.h" #include "third_party/blink/renderer/core/editing/position.h" #include "third_party/blink/renderer/core/layout/layout_text_fragment.h" -#include "third_party/blink/renderer/core/layout/ng/inline/ng_caret_navigator.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h" #include "third_party/blink/renderer/core/layout/ng/ng_block_node.h" #include "third_party/blink/renderer/platform/text/character.h" @@ -27,16 +26,16 @@ bool CanUseNGOffsetMapping(const LayoutObject& object) { } Position CreatePositionForOffsetMapping(const Node& node, unsigned dom_offset) { - if (node.IsTextNode()) { + if (auto* text_node = DynamicTo<Text>(node)) { // 'text-transform' may make the rendered text length longer than the // original text node, in which case we clamp the offset to avoid crashing. // TODO(crbug.com/750990): Support 'text-transform' to remove this hack. #if DCHECK_IS_ON() // Ensures that the clamping hack kicks in only with text-transform. if (node.ComputedStyleRef().TextTransform() == ETextTransform::kNone) - DCHECK_LE(dom_offset, ToText(node).length()); + DCHECK_LE(dom_offset, text_node->length()); #endif - const unsigned clamped_offset = std::min(dom_offset, ToText(node).length()); + const unsigned clamped_offset = std::min(dom_offset, text_node->length()); return Position(&node, clamped_offset); } // For non-text-anchored position, the offset must be either 0 or 1. @@ -46,13 +45,13 @@ Position CreatePositionForOffsetMapping(const Node& node, unsigned dom_offset) { std::pair<const Node&, unsigned> ToNodeOffsetPair(const Position& position) { DCHECK(NGOffsetMapping::AcceptsPosition(position)) << position; - if (position.AnchorNode()->IsTextNode()) { + if (auto* text_node = DynamicTo<Text>(position.AnchorNode())) { if (position.IsOffsetInAnchor()) return {*position.AnchorNode(), position.OffsetInContainerNode()}; if (position.IsBeforeAnchor()) return {*position.AnchorNode(), 0}; DCHECK(position.IsAfterAnchor()); - return {*position.AnchorNode(), ToText(position.AnchorNode())->length()}; + return {*position.AnchorNode(), text_node->length()}; } if (position.IsBeforeAnchor()) return {*position.AnchorNode(), 0}; @@ -99,7 +98,29 @@ NGOffsetMappingUnit::NGOffsetMappingUnit(NGOffsetMappingUnitType type, dom_start_(dom_start), dom_end_(dom_end), text_content_start_(text_content_start), - text_content_end_(text_content_end) {} + text_content_end_(text_content_end) { + AssertValid(); +} + +void NGOffsetMappingUnit::AssertValid() const { +#if ENABLE_SECURITY_ASSERT + SECURITY_DCHECK(dom_start_ <= dom_end_) << dom_start_ << " vs. " << dom_end_; + SECURITY_DCHECK(text_content_start_ <= text_content_end_) + << text_content_start_ << " vs. " << text_content_end_; + if (layout_object_->IsText()) { + const LayoutText& layout_text = ToLayoutText(*layout_object_); + const unsigned text_start = + AssociatedNode() ? layout_text.TextStartOffset() : 0; + const unsigned text_end = text_start + layout_text.TextLength(); + SECURITY_DCHECK(dom_end_ >= text_start) + << dom_end_ << " vs. " << text_start; + SECURITY_DCHECK(dom_end_ <= text_end) << dom_end_ << " vs. " << text_end; + } else { + SECURITY_DCHECK(dom_start_ == 0) << dom_start_; + SECURITY_DCHECK(dom_end_ == 1) << dom_end_; + } +#endif +} NGOffsetMappingUnit::~NGOffsetMappingUnit() = default; @@ -238,7 +259,23 @@ LayoutBlockFlow* NGOffsetMapping::GetInlineFormattingContextOf( NGOffsetMapping::NGOffsetMapping(UnitVector&& units, RangeMap&& ranges, String text) - : units_(std::move(units)), ranges_(std::move(ranges)), text_(text) {} + : units_(std::move(units)), ranges_(std::move(ranges)), text_(text) { +#if ENABLE_SECURITY_ASSERT + for (const auto& unit : units_) { + SECURITY_DCHECK(unit.TextContentStart() <= text.length()) + << unit.TextContentStart() << "<=" << text.length(); + SECURITY_DCHECK(unit.TextContentEnd() <= text.length()) + << unit.TextContentEnd() << "<=" << text.length(); + unit.AssertValid(); + } + for (const auto& pair : ranges) { + SECURITY_DCHECK(pair.value.first < units_.size()) + << pair.value.first << "<" << units_.size(); + SECURITY_DCHECK(pair.value.second < units_.size()) + << pair.value.second << "<" << units_.size(); + } +#endif +} NGOffsetMapping::~NGOffsetMapping() = default; @@ -385,7 +422,7 @@ Position NGOffsetMapping::StartOfNextNonCollapsedContent( const auto node_and_offset = ToNodeOffsetPair(position); const Node& node = node_and_offset.first; const unsigned offset = node_and_offset.second; - while (unit != units_.end() && unit->GetOwner() == node) { + while (unit != units_.end() && unit->AssociatedNode() == node) { if (unit->DOMEnd() > offset && unit->GetType() != NGOffsetMappingUnitType::kCollapsed) { const unsigned result = std::max(offset, unit->DOMStart()); @@ -406,7 +443,7 @@ Position NGOffsetMapping::EndOfLastNonCollapsedContent( const auto node_and_offset = ToNodeOffsetPair(position); const Node& node = node_and_offset.first; const unsigned offset = node_and_offset.second; - while (unit->GetOwner() == node) { + while (unit->AssociatedNode() == node) { if (unit->DOMStart() < offset && unit->GetType() != NGOffsetMappingUnitType::kCollapsed) { const unsigned result = std::min(offset, unit->DOMEnd()); @@ -509,16 +546,6 @@ Position NGOffsetMapping::GetLastPosition(unsigned offset) const { return CreatePositionForOffsetMapping(node, dom_offset); } -PositionWithAffinity NGOffsetMapping::GetPositionWithAffinity( - const NGCaretNavigator::Position& position) const { - if (position.IsBeforeCharacter()) { - return PositionWithAffinity(GetLastPosition(position.index), - TextAffinity::kDownstream); - } - return PositionWithAffinity(GetFirstPosition(position.index + 1), - TextAffinity::kUpstream); -} - bool NGOffsetMapping::HasBidiControlCharactersOnly(unsigned start, unsigned end) const { DCHECK_LE(start, end); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h index a8b59f75f1e..743dd9d8757 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h @@ -9,7 +9,6 @@ #include "third_party/blink/renderer/core/core_export.h" #include "third_party/blink/renderer/core/dom/node.h" #include "third_party/blink/renderer/core/editing/forward.h" -#include "third_party/blink/renderer/core/layout/ng/inline/ng_caret_navigator.h" #include "third_party/blink/renderer/platform/heap/handle.h" #include "third_party/blink/renderer/platform/heap/persistent.h" #include "third_party/blink/renderer/platform/wtf/allocator.h" @@ -73,6 +72,8 @@ class CORE_EXPORT NGOffsetMappingUnit { unsigned ConvertTextContentToFirstDOMOffset(unsigned) const; unsigned ConvertTextContentToLastDOMOffset(unsigned) const; + void AssertValid() const; + private: NGOffsetMappingUnitType type_ = NGOffsetMappingUnitType::kIdentity; @@ -82,6 +83,10 @@ class CORE_EXPORT NGOffsetMappingUnit { // offset in |LayoutText::text_| instead of DOM node. unsigned dom_start_; unsigned dom_end_; + + // |text_content_start_| and |text_content_end_| are offsets in + // |NGOffsetMapping::text_|. These values are in [0, |text_.length()] to + // represent collapsed spaces at the end of block. unsigned text_content_start_; unsigned text_content_end_; @@ -219,13 +224,6 @@ class CORE_EXPORT NGOffsetMapping { Position GetFirstPosition(unsigned) const; Position GetLastPosition(unsigned) const; - // Converts the given caret position on text content to a PositionWithAffinity - // in DOM. If |position| is before a character, the function creates a - // downstream position before |GetLastPosition()| of the character; otherwise, - // it returns an upstream position after |GetFirstPosition()| of the character - PositionWithAffinity GetPositionWithAffinity( - const NGCaretNavigator::Position& position) const; - // Returns all NGOffsetMappingUnits whose text content ranges has non-empty // (but possibly collapsed) intersection with (start, end). Note that units // that only "touch" |start| or |end| are excluded. diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping_builder.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping_builder.cc index 8969d67f8f5..9524bfb97ac 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping_builder.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping_builder.cc @@ -7,7 +7,6 @@ #include <utility> #include "third_party/blink/renderer/core/layout/layout_text.h" #include "third_party/blink/renderer/core/layout/layout_text_fragment.h" -#include "third_party/blink/renderer/core/layout/ng/inline/ng_caret_navigator.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h" namespace blink { diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping_test.cc index f41b357b915..78b74c7b685 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping_test.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping_test.cc @@ -1225,6 +1225,33 @@ TEST_F(NGOffsetMappingTest, SoftHyphen) { TEST_RANGE(mapping.GetRanges(), text, 0u, 1u); } +// For http://crbug.com/965353 +TEST_F(NGOffsetMappingTest, PreWrapAndReusing) { + // Note: "white-space: break-space" yields same result. + SetupHtml("t", "<p id='t' style='white-space: pre-wrap'>abc</p>"); + Element& target = *GetDocument().getElementById("t"); + + // Change to <p id=t>abc xyz</p> + Text& text = *Text::Create(GetDocument(), " xyz"); + target.appendChild(&text); + UpdateAllLifecyclePhasesForTest(); + + // Change to <p id=t> xyz</p>. We attempt to reuse " xyz". + target.firstChild()->remove(); + UpdateAllLifecyclePhasesForTest(); + + const NGOffsetMapping& mapping = GetOffsetMapping(); + EXPECT_EQ(String(u" \u200Bxyz"), mapping.GetText()) + << "We have ZWS after leading preserved space."; + EXPECT_EQ((Vector<NGOffsetMappingUnit>{ + NGOffsetMappingUnit(kIdentity, *text.GetLayoutObject(), 0u, 1u, + 0u, 1u), + NGOffsetMappingUnit(kIdentity, *text.GetLayoutObject(), 1u, 4u, + 2u, 5u), + }), + mapping.GetUnits()); +} + TEST_F(NGOffsetMappingTest, TextOverflowEllipsis) { LoadAhem(); SetupHtml("t", @@ -1241,6 +1268,41 @@ TEST_F(NGOffsetMappingTest, TextOverflowEllipsis) { TEST_RANGE(mapping.GetRanges(), text, 0u, 1u); } +// https://crbug.com/967106 +TEST_F(NGOffsetMappingTest, StartOfNextNonCollapsedContentWithPseudo) { + // The white spaces are necessary for bug repro. Do not remove them. + SetupHtml("t", R"HTML( + <style>span#quote::before { content: '"'}</style> + <div id=t> + <span>foo </span> + <span id=quote>bar</span> + </div>)HTML"); + + const Element* quote = GetElementById("quote"); + const Node* text = quote->previousSibling(); + const Position position = Position::FirstPositionInNode(*text); + + EXPECT_EQ(Position(), + GetOffsetMapping().StartOfNextNonCollapsedContent(position)); +} + +// https://crbug.com/967106 +TEST_F(NGOffsetMappingTest, EndOfLastNonCollapsedContentWithPseudo) { + // The white spaces are necessary for bug repro. Do not remove them. + SetupHtml("t", R"HTML( + <style>span#quote::after { content: '" '}</style> + <div id=t> + <span id=quote>foo</span> + <span>bar</span> + </div>)HTML"); + + const Element* quote = GetElementById("quote"); + const Node* text = quote->nextSibling(); + const Position position = Position::LastPositionInNode(*text); + + EXPECT_EQ(Position(), + GetOffsetMapping().EndOfLastNonCollapsedContent(position)); +} // Test |GetOffsetMapping| which is available both for LayoutNG and for legacy. class NGOffsetMappingGetterTest : public RenderingTest, public testing::WithParamInterface<bool>, 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 bcbbeb51ba0..08681856ab1 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 @@ -4,7 +4,9 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h" +#include "third_party/blink/renderer/core/editing/editing_utilities.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_fragment_traversal.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment_builder.h" #include "third_party/blink/renderer/core/layout/ng/ng_fragment.h" #include "third_party/blink/renderer/core/layout/ng/ng_relative_utils.h" @@ -15,7 +17,6 @@ namespace blink { namespace { struct SameSizeAsNGPhysicalLineBoxFragment : NGPhysicalContainerFragment { - void* pointer; NGLineHeightMetrics metrics; }; @@ -23,6 +24,18 @@ static_assert(sizeof(NGPhysicalLineBoxFragment) == sizeof(SameSizeAsNGPhysicalLineBoxFragment), "NGPhysicalLineBoxFragment should stay small"); +bool IsInlineLeaf(const NGPhysicalFragment& fragment) { + if (fragment.IsText()) + return true; + return fragment.IsBox() && fragment.IsAtomicInline(); +} + +bool IsEditableFragment(const NGPhysicalFragment& fragment) { + if (!fragment.GetNode()) + return false; + return HasEditableStyle(*fragment.GetNode()); +} + } // namespace scoped_refptr<const NGPhysicalLineBoxFragment> @@ -34,7 +47,7 @@ NGPhysicalLineBoxFragment::Create(NGLineBoxFragmentBuilder* builder) { // we pass the buffer as a constructor argument. void* data = ::WTF::Partitions::FastMalloc( sizeof(NGPhysicalLineBoxFragment) + - builder->children_.size() * sizeof(NGLinkStorage), + builder->children_.size() * sizeof(NGLink), ::WTF::GetStringWithTypeName<NGPhysicalLineBoxFragment>()); new (data) NGPhysicalLineBoxFragment(builder); return base::AdoptRef(static_cast<NGPhysicalLineBoxFragment*>(data)); @@ -48,8 +61,13 @@ NGPhysicalLineBoxFragment::NGPhysicalLineBoxFragment( kFragmentLineBox, builder->line_box_type_), metrics_(builder->metrics_) { - style_ = std::move(builder->style_); + // A line box must have a metrics unless it's an empty line box. + DCHECK(!metrics_.IsEmpty() || IsEmptyLineBox()); base_direction_ = static_cast<unsigned>(builder->base_direction_); + has_hanging_ = builder->hang_inline_size_ != 0; + has_propagated_descendants_ = has_floating_descendants_ || + HasOutOfFlowPositionedDescendants() || + builder->unpositioned_list_marker_; } NGLineHeightMetrics NGPhysicalLineBoxFragment::BaselineMetrics( @@ -60,17 +78,35 @@ NGLineHeightMetrics NGPhysicalLineBoxFragment::BaselineMetrics( return metrics_; } -NGPhysicalOffsetRect NGPhysicalLineBoxFragment::ScrollableOverflow( +PhysicalRect NGPhysicalLineBoxFragment::ScrollableOverflow( const LayoutObject* container, const ComputedStyle* container_style, - NGPhysicalSize container_physical_size) const { + PhysicalSize container_physical_size) const { WritingMode container_writing_mode = container_style->GetWritingMode(); TextDirection container_direction = container_style->Direction(); - NGPhysicalOffsetRect overflow({}, Size()); + PhysicalRect overflow({}, Size()); for (const auto& child : Children()) { - NGPhysicalOffsetRect child_scroll_overflow = + PhysicalRect child_scroll_overflow = child->ScrollableOverflowForPropagation(container); child_scroll_overflow.offset += child.Offset(); + + // Chop the hanging part from scrollable overflow. Children overflow in + // inline direction should hang, which should not cause scroll. + // TODO(kojii): Should move to text fragment to make this more accurate. + if (UNLIKELY(has_hanging_ && !child->IsFloatingOrOutOfFlowPositioned())) { + if (IsHorizontalWritingMode(container_writing_mode)) { + if (child_scroll_overflow.offset.left < 0) + child_scroll_overflow.offset.left = LayoutUnit(); + if (child_scroll_overflow.Right() > Size().width) + child_scroll_overflow.ShiftRightEdgeTo(Size().width); + } else { + if (child_scroll_overflow.offset.top < 0) + child_scroll_overflow.offset.top = LayoutUnit(); + if (child_scroll_overflow.Bottom() > Size().height) + child_scroll_overflow.ShiftBottomEdgeTo(Size().height); + } + } + // If child has the same style as parent, parent will compute relative // offset. if (&child->Style() != container_style) { @@ -130,20 +166,63 @@ bool NGPhysicalLineBoxFragment::HasSoftWrapToNextLine() const { return !break_token.IsFinished() && !break_token.IsForcedBreak(); } -NGPhysicalOffset NGPhysicalLineBoxFragment::LineStartPoint() const { - const NGLogicalOffset logical_start; // (0, 0) - const NGPhysicalSize pixel_size(LayoutUnit(1), LayoutUnit(1)); +PhysicalOffset NGPhysicalLineBoxFragment::LineStartPoint() const { + const LogicalOffset logical_start; // (0, 0) + const PhysicalSize pixel_size(LayoutUnit(1), LayoutUnit(1)); return logical_start.ConvertToPhysical(Style().GetWritingMode(), BaseDirection(), Size(), pixel_size); } -NGPhysicalOffset NGPhysicalLineBoxFragment::LineEndPoint() const { +PhysicalOffset NGPhysicalLineBoxFragment::LineEndPoint() const { const LayoutUnit inline_size = NGFragment(Style().GetWritingMode(), *this).InlineSize(); - const NGLogicalOffset logical_end(inline_size, LayoutUnit()); - const NGPhysicalSize pixel_size(LayoutUnit(1), LayoutUnit(1)); + const LogicalOffset logical_end(inline_size, LayoutUnit()); + const PhysicalSize pixel_size(LayoutUnit(1), LayoutUnit(1)); return logical_end.ConvertToPhysical(Style().GetWritingMode(), BaseDirection(), Size(), pixel_size); } +const LayoutObject* NGPhysicalLineBoxFragment::ClosestLeafChildForPoint( + const PhysicalOffset& point, + bool only_editable_leaves) const { + const PhysicalSize unit_square(LayoutUnit(1), LayoutUnit(1)); + const LogicalOffset logical_point = point.ConvertToLogical( + Style().GetWritingMode(), BaseDirection(), Size(), unit_square); + const LayoutUnit inline_offset = logical_point.inline_offset; + const NGPhysicalFragment* closest_leaf_child = nullptr; + LayoutUnit closest_leaf_distance; + for (const auto& descendant : + NGInlineFragmentTraversal::DescendantsOf(*this)) { + const NGPhysicalFragment& fragment = *descendant.fragment; + if (!fragment.GetLayoutObject()) + continue; + if (!IsInlineLeaf(fragment) || fragment.IsListMarker()) + continue; + if (only_editable_leaves && !IsEditableFragment(fragment)) + continue; + + const LogicalSize fragment_logical_size = + fragment.Size().ConvertToLogical(Style().GetWritingMode()); + const LogicalOffset fragment_logical_offset = + descendant.offset_to_container_box.ConvertToLogical( + Style().GetWritingMode(), BaseDirection(), Size(), fragment.Size()); + const LayoutUnit inline_min = fragment_logical_offset.inline_offset; + const LayoutUnit inline_max = fragment_logical_offset.inline_offset + + fragment_logical_size.inline_size; + if (inline_offset >= inline_min && inline_offset < inline_max) + return fragment.GetLayoutObject(); + + const LayoutUnit distance = + inline_offset < inline_min ? inline_min - inline_offset + : inline_offset - inline_max + LayoutUnit(1); + if (!closest_leaf_child || distance < closest_leaf_distance) { + closest_leaf_child = &fragment; + closest_leaf_distance = distance; + } + } + if (!closest_leaf_child) + return nullptr; + return closest_leaf_child->GetLayoutObject(); +} + } // namespace blink 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 a295aacc7a3..7a453e31e5d 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 @@ -32,7 +32,7 @@ class CORE_EXPORT NGPhysicalLineBoxFragment final NGLineBoxFragmentBuilder* builder); ~NGPhysicalLineBoxFragment() { - for (const NGLinkStorage& child : Children()) + for (const NGLink& child : Children()) child.fragment->Release(); } @@ -41,8 +41,9 @@ class CORE_EXPORT NGPhysicalLineBoxFragment final } bool IsEmptyLineBox() const { return LineBoxType() == kEmptyLineBox; } + // True if descendants were propagated to outside of this fragment. + bool HasPropagatedDescendants() const { return has_propagated_descendants_; } - const ComputedStyle& Style() const { return *style_; } const NGLineHeightMetrics& Metrics() const { return metrics_; } // The base direction of this line. Also known as the paragraph direction. @@ -59,20 +60,22 @@ class CORE_EXPORT NGPhysicalLineBoxFragment final // 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 LayoutObject* container, - const ComputedStyle* container_style, - NGPhysicalSize container_physical_size) const; + PhysicalRect ScrollableOverflow(const LayoutObject* container, + const ComputedStyle* container_style, + PhysicalSize 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; + const LayoutObject* ClosestLeafChildForPoint(const PhysicalOffset&, + bool only_editable_leaves) const; + // Returns a point at the visual start/end of the line. // Encapsulates the handling of text direction and writing mode. - NGPhysicalOffset LineStartPoint() const; - NGPhysicalOffset LineEndPoint() const; + PhysicalOffset LineStartPoint() const; + PhysicalOffset LineEndPoint() const; // Whether the content soft-wraps to the next line. bool HasSoftWrapToNextLine() const; @@ -80,9 +83,8 @@ class CORE_EXPORT NGPhysicalLineBoxFragment final private: NGPhysicalLineBoxFragment(NGLineBoxFragmentBuilder* builder); - scoped_refptr<const ComputedStyle> style_; NGLineHeightMetrics metrics_; - NGLinkStorage children_[]; + NGLink children_[]; }; template <> 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 26ca7c236bf..53167624896 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 @@ -16,7 +16,7 @@ class NGPhysicalLineBoxFragmentTest : public NGLayoutTest { NGPhysicalLineBoxFragmentTest() : NGLayoutTest() {} protected: - const NGPhysicalLineBoxFragment* GetLineBox() const { + Vector<const NGPhysicalLineBoxFragment*> GetLineBoxes() const { const Element* container = GetElementById("root"); DCHECK(container); const LayoutObject* layout_object = container->GetLayoutObject(); @@ -26,12 +26,21 @@ class NGPhysicalLineBoxFragmentTest : public NGLayoutTest { To<LayoutBlockFlow>(layout_object)->CurrentFragment(); DCHECK(root_fragment) << container; + Vector<const NGPhysicalLineBoxFragment*> lines; for (const auto& child : NGInlineFragmentTraversal::DescendantsOf(*root_fragment)) { - if (child.fragment->IsLineBox()) - return To<NGPhysicalLineBoxFragment>(child.fragment.get()); + if (const NGPhysicalLineBoxFragment* line = + DynamicTo<NGPhysicalLineBoxFragment>(child.fragment.get())) { + lines.push_back(line); + } } - NOTREACHED(); + return lines; + } + + const NGPhysicalLineBoxFragment* GetLineBox() const { + Vector<const NGPhysicalLineBoxFragment*> lines = GetLineBoxes(); + if (!lines.IsEmpty()) + return lines.front(); return nullptr; } }; @@ -47,6 +56,42 @@ class NGPhysicalLineBoxFragmentTest : public NGLayoutTest { EXPECT_EQ(GetElementById(id), fragment->GetNode()); \ } +TEST_F(NGPhysicalLineBoxFragmentTest, HasPropagatedDescendantsFloat) { + SetBodyInnerHTML(R"HTML( + <!DOCTYPE html> + <style> + div { + font-size: 10px; + width: 10ch; + } + .float { float: left; } + </style> + <div id=root>12345678 12345<div class=float>float</div></div> + )HTML"); + Vector<const NGPhysicalLineBoxFragment*> lines = GetLineBoxes(); + EXPECT_EQ(lines.size(), 2u); + EXPECT_FALSE(lines[0]->HasPropagatedDescendants()); + EXPECT_TRUE(lines[1]->HasPropagatedDescendants()); +} + +TEST_F(NGPhysicalLineBoxFragmentTest, HasPropagatedDescendantsOOF) { + SetBodyInnerHTML(R"HTML( + <!DOCTYPE html> + <style> + div { + font-size: 10px; + width: 10ch; + } + .abspos { position: absolute; } + </style> + <div id=root>12345678 12345<div class=abspos>abspos</div></div> + )HTML"); + Vector<const NGPhysicalLineBoxFragment*> lines = GetLineBoxes(); + EXPECT_EQ(lines.size(), 2u); + EXPECT_FALSE(lines[0]->HasPropagatedDescendants()); + EXPECT_TRUE(lines[1]->HasPropagatedDescendants()); +} + TEST_F(NGPhysicalLineBoxFragmentTest, FirstLastLogicalLeafInSimpleText) { SetBodyInnerHTML( "<div id=root>" 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 6db5844ca0f..384bb007a72 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 @@ -5,12 +5,13 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h" #include "third_party/blink/renderer/core/dom/node.h" +#include "third_party/blink/renderer/core/layout/geometry/logical_size.h" +#include "third_party/blink/renderer/core/layout/geometry/physical_rect.h" #include "third_party/blink/renderer/core/layout/layout_text_fragment.h" #include "third_party/blink/renderer/core/layout/line/line_orientation_utils.h" -#include "third_party/blink/renderer/core/layout/ng/geometry/ng_logical_size.h" -#include "third_party/blink/renderer/core/layout/ng/geometry/ng_physical_offset_rect.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.h" +#include "third_party/blink/renderer/core/page/page.h" #include "third_party/blink/renderer/core/style/computed_style.h" #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_view.h" @@ -19,24 +20,15 @@ namespace blink { namespace { struct SameSizeAsNGPhysicalTextFragment : NGPhysicalFragment { - void* pointers[3]; + void* pointers[2]; unsigned offsets[2]; + PhysicalRect rect; }; static_assert(sizeof(NGPhysicalTextFragment) == sizeof(SameSizeAsNGPhysicalTextFragment), "NGPhysicalTextFragment should stay small"); -inline bool IsPhysicalTextFragmentAnonymousText( - const LayoutObject* layout_object) { - if (!layout_object) - return false; - if (layout_object->IsText() && ToLayoutText(layout_object)->IsTextFragment()) - return !ToLayoutTextFragment(layout_object)->AssociatedTextNode(); - const Node* node = layout_object->GetNode(); - return !node || node->IsPseudoElement(); -} - NGLineOrientation ToLineOrientation(WritingMode writing_mode) { switch (writing_mode) { case WritingMode::kHorizontalTb: @@ -59,15 +51,14 @@ NGPhysicalTextFragment::NGPhysicalTextFragment( unsigned start_offset, unsigned end_offset, scoped_refptr<const ShapeResultView> shape_result) - : NGPhysicalFragment(source.GetLayoutObject(), - source.StyleVariant(), - source.IsHorizontal() - ? NGPhysicalSize{shape_result->SnappedWidth(), - source.Size().height} - : NGPhysicalSize{source.Size().width, - shape_result->SnappedWidth()}, - kFragmentText, - source.TextType()), + : NGPhysicalFragment( + source.GetMutableLayoutObject(), + source.StyleVariant(), + source.IsHorizontal() + ? PhysicalSize{shape_result->SnappedWidth(), source.Size().height} + : PhysicalSize{source.Size().width, shape_result->SnappedWidth()}, + kFragmentText, + source.TextType()), text_(source.text_), start_offset_(start_offset), end_offset_(end_offset), @@ -75,11 +66,9 @@ NGPhysicalTextFragment::NGPhysicalTextFragment( DCHECK_GE(start_offset_, source.StartOffset()); DCHECK_LE(end_offset_, source.EndOffset()); DCHECK(shape_result_ || IsFlowControl()) << ToString(); - DCHECK(!source.rare_data_ || !source.rare_data_->style_); line_orientation_ = source.line_orientation_; - is_anonymous_text_ = source.is_anonymous_text_; - - UpdateSelfInkOverflow(); + is_generated_text_ = source.is_generated_text_; + ink_overflow_computed_ = false; } NGPhysicalTextFragment::NGPhysicalTextFragment(NGTextFragmentBuilder* builder) @@ -91,53 +80,25 @@ NGPhysicalTextFragment::NGPhysicalTextFragment(NGTextFragmentBuilder* builder) DCHECK(shape_result_ || IsFlowControl()) << ToString(); line_orientation_ = static_cast<unsigned>(ToLineOrientation(builder->GetWritingMode())); - - if (UNLIKELY(StyleVariant() == NGStyleVariant::kEllipsis)) { - EnsureRareData()->style_ = std::move(builder->style_); - is_anonymous_text_ = true; - } else { - is_anonymous_text_ = - builder->text_type_ == kGeneratedText || - IsPhysicalTextFragmentAnonymousText(builder->layout_object_); - } - - UpdateSelfInkOverflow(); -} - -NGPhysicalTextFragment::RareData* NGPhysicalTextFragment::EnsureRareData() { - if (!rare_data_) - rare_data_ = std::make_unique<RareData>(); - return rare_data_.get(); -} - -const ComputedStyle& NGPhysicalTextFragment::Style() const { - switch (StyleVariant()) { - case NGStyleVariant::kStandard: - case NGStyleVariant::kFirstLine: - return NGPhysicalFragment::Style(); - case NGStyleVariant::kEllipsis: - DCHECK(rare_data_ && rare_data_->style_); - return *rare_data_->style_; - } - NOTREACHED(); - return NGPhysicalFragment::Style(); + is_generated_text_ = builder->IsGeneratedText(); + ink_overflow_computed_ = false; } // Convert logical cooridnate to local physical coordinate. -NGPhysicalOffsetRect NGPhysicalTextFragment::ConvertToLocal( +PhysicalRect NGPhysicalTextFragment::ConvertToLocal( const LayoutRect& logical_rect) const { switch (LineOrientation()) { case NGLineOrientation::kHorizontal: - return NGPhysicalOffsetRect(logical_rect); + return PhysicalRect(logical_rect); case NGLineOrientation::kClockWiseVertical: - return {{size_.width - logical_rect.MaxY(), logical_rect.X()}, - {logical_rect.Height(), logical_rect.Width()}}; + return {size_.width - logical_rect.MaxY(), logical_rect.X(), + logical_rect.Height(), logical_rect.Width()}; case NGLineOrientation::kCounterClockWiseVertical: - return {{logical_rect.Y(), size_.height - logical_rect.MaxX()}, - {logical_rect.Height(), logical_rect.Width()}}; + return {logical_rect.Y(), size_.height - logical_rect.MaxX(), + logical_rect.Height(), logical_rect.Width()}; } NOTREACHED(); - return NGPhysicalOffsetRect(logical_rect); + return PhysicalRect(logical_rect); } // Compute the inline position from text offset, in logical coordinate relative @@ -190,9 +151,8 @@ NGPhysicalTextFragment::LineLeftAndRightForOffsets(unsigned start_offset, : std::make_pair(start_position, end_position); } -NGPhysicalOffsetRect NGPhysicalTextFragment::LocalRect( - unsigned start_offset, - unsigned end_offset) const { +PhysicalRect NGPhysicalTextFragment::LocalRect(unsigned start_offset, + unsigned end_offset) const { if (start_offset == start_offset_ && end_offset == end_offset_) return LocalRect(); LayoutUnit start_position, end_position; @@ -201,34 +161,38 @@ NGPhysicalOffsetRect NGPhysicalTextFragment::LocalRect( const LayoutUnit inline_size = end_position - start_position; switch (LineOrientation()) { case NGLineOrientation::kHorizontal: - return {{start_position, LayoutUnit()}, {inline_size, Size().height}}; + return {start_position, LayoutUnit(), inline_size, Size().height}; case NGLineOrientation::kClockWiseVertical: - return {{LayoutUnit(), start_position}, {Size().width, inline_size}}; + return {LayoutUnit(), start_position, Size().width, inline_size}; case NGLineOrientation::kCounterClockWiseVertical: - return {{LayoutUnit(), Size().height - end_position}, - {Size().width, inline_size}}; + return {LayoutUnit(), Size().height - end_position, Size().width, + inline_size}; } NOTREACHED(); return {}; } -NGPhysicalOffsetRect NGPhysicalTextFragment::SelfInkOverflow() const { - return UNLIKELY(rare_data_) ? rare_data_->self_ink_overflow_ : LocalRect(); +PhysicalRect NGPhysicalTextFragment::SelfInkOverflow() const { + if (!ink_overflow_computed_) + ComputeSelfInkOverflow(); + return self_ink_overflow_; } -void NGPhysicalTextFragment::ClearSelfInkOverflow() { - if (UNLIKELY(rare_data_)) - rare_data_->self_ink_overflow_ = LocalRect(); +void NGPhysicalTextFragment::ClearSelfInkOverflow() const { + self_ink_overflow_ = LocalRect(); } -void NGPhysicalTextFragment::UpdateSelfInkOverflow() { +void NGPhysicalTextFragment::ComputeSelfInkOverflow() const { + ink_overflow_computed_ = true; + if (UNLIKELY(!shape_result_)) { ClearSelfInkOverflow(); return; } // Glyph bounds is in logical coordinate, origin at the alphabetic baseline. - LayoutRect ink_overflow = EnclosingLayoutRect(shape_result_->Bounds()); + FloatRect text_ink_bounds = Style().GetFont().TextInkBounds(PaintInfo()); + LayoutRect ink_overflow = EnclosingLayoutRect(text_ink_bounds); // Make the origin at the logical top of this fragment. const ComputedStyle& style = Style(); @@ -269,18 +233,18 @@ void NGPhysicalTextFragment::UpdateSelfInkOverflow() { // 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_ink_overflow = ConvertToLocal(ink_overflow); - NGPhysicalOffsetRect local_rect = LocalRect(); + PhysicalRect local_ink_overflow = ConvertToLocal(ink_overflow); + PhysicalRect local_rect = LocalRect(); if (local_rect.Contains(local_ink_overflow)) { - ClearSelfInkOverflow(); + self_ink_overflow_ = local_rect; return; } local_ink_overflow.Unite(local_rect); local_ink_overflow.ExpandEdgesToPixelBoundaries(); - EnsureRareData()->self_ink_overflow_ = local_ink_overflow; + self_ink_overflow_ = local_ink_overflow; } -scoped_refptr<const NGPhysicalFragment> NGPhysicalTextFragment::TrimText( +scoped_refptr<const NGPhysicalTextFragment> NGPhysicalTextFragment::TrimText( unsigned new_start_offset, unsigned new_end_offset) const { DCHECK(shape_result_); @@ -294,7 +258,7 @@ scoped_refptr<const NGPhysicalFragment> NGPhysicalTextFragment::TrimText( } unsigned NGPhysicalTextFragment::TextOffsetForPoint( - const NGPhysicalOffset& point) const { + const PhysicalOffset& point) const { const ComputedStyle& style = Style(); const LayoutUnit& point_in_line_direction = style.IsHorizontalWritingMode() ? point.left : point.top; @@ -311,7 +275,7 @@ unsigned NGPhysicalTextFragment::TextOffsetForPoint( DCHECK(IsFlowControl()); // Zero-inline-size objects such as newline always return the start offset. - NGLogicalSize size = Size().ConvertToLogical(style.GetWritingMode()); + LogicalSize size = Size().ConvertToLogical(style.GetWritingMode()); if (!size.inline_size) return 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 c3f7669da89..c46013f2bc8 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 @@ -17,8 +17,9 @@ namespace blink { -struct NGPhysicalOffsetRect; +struct PhysicalRect; class NGTextFragmentBuilder; +class NGPhysicalTextFragment; enum class AdjustMidCluster; @@ -54,7 +55,7 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { // |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. + // are not classified to this. See IsGeneratedText() for them. kGeneratedText, // When adding new values, make sure the bit size of |sub_type_| is large // enough to store. @@ -63,8 +64,9 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { NGPhysicalTextFragment(NGTextFragmentBuilder*); NGTextType TextType() const { return static_cast<NGTextType>(sub_type_); } - // True if this is a generated text. - bool IsGeneratedText() const { return TextType() == kGeneratedText; } + // Returns true if the text is generated (from, e.g., list marker, + // pseudo-element, ...) instead of from a DOM text node. + bool IsGeneratedText() const { return is_generated_text_; } // 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, @@ -72,8 +74,10 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { bool IsFlowControl() const { return IsLineBreak() || TextType() == kFlowControl; } - - const ComputedStyle& Style() const; + // True if this is an ellpisis generated by `text-overflow: ellipsis`. + bool IsEllipsis() const { + return StyleVariant() == NGStyleVariant::kEllipsis; + } unsigned Length() const { return end_offset_ - start_offset_; } StringView Text() const { return StringView(text_, start_offset_, Length()); } @@ -99,18 +103,18 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { // The layout box of text in (start, end) range in local coordinate. // Start and end offsets must be between StartOffset() and EndOffset(). - NGPhysicalOffsetRect LocalRect(unsigned start_offset, - unsigned end_offset) const; + PhysicalRect LocalRect(unsigned start_offset, unsigned end_offset) const; using NGPhysicalFragment::LocalRect; // The visual bounding box that includes glpyh bounding box and CSS // properties, in local coordinates. - NGPhysicalOffsetRect SelfInkOverflow() const; + PhysicalRect SelfInkOverflow() const; // Create a new fragment that has part of the text of this fragment. // All other properties are the same as this fragment. - scoped_refptr<const NGPhysicalFragment> TrimText(unsigned start_offset, - unsigned end_offset) const; + scoped_refptr<const NGPhysicalTextFragment> TrimText( + unsigned start_offset, + unsigned end_offset) const; scoped_refptr<const NGPhysicalFragment> CloneWithoutOffset() const; @@ -119,12 +123,8 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { TextShapeResult()}; } - // Returns true if the text is generated (from, e.g., list marker, - // pseudo-element, ...) instead of from a DOM text node. - bool IsAnonymousText() const { return is_anonymous_text_; } - // Returns the text offset in the fragment placed closest to the given point. - unsigned TextOffsetForPoint(const NGPhysicalOffset&) const; + unsigned TextOffsetForPoint(const PhysicalOffset&) const; UBiDiLevel BidiLevel() const; TextDirection ResolvedDirection() const; @@ -143,23 +143,14 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { unsigned end_offset, scoped_refptr<const ShapeResultView> shape_result); - struct RareData { - USING_FAST_MALLOC(RareData); - - public: - NGPhysicalOffsetRect self_ink_overflow_; - scoped_refptr<const ComputedStyle> style_; // Used only for ellipsis. - }; - RareData* EnsureRareData(); - LayoutUnit InlinePositionForOffset(unsigned offset, LayoutUnit (*round)(float), AdjustMidCluster) const; - NGPhysicalOffsetRect ConvertToLocal(const LayoutRect&) const; + PhysicalRect ConvertToLocal(const LayoutRect&) const; - void UpdateSelfInkOverflow(); - void ClearSelfInkOverflow(); + void ComputeSelfInkOverflow() const; + void ClearSelfInkOverflow() const; // The text of NGInlineNode; i.e., of a parent block. The text for this // fragment is a substring(start_offset_, end_offset_) of this string. @@ -170,7 +161,10 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { const unsigned end_offset_; const scoped_refptr<const ShapeResultView> shape_result_; - std::unique_ptr<RareData> rare_data_; + // Fragments are immutable but allow certain expensive data, specifically ink + // overflow, to be cached as long as it is guaranteed to always recompute to + // the same value. + mutable PhysicalRect self_ink_overflow_; }; template <> 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 ce4035dd595..1867897f0d7 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 @@ -4,7 +4,7 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h" -#include "third_party/blink/renderer/core/layout/ng/geometry/ng_logical_rect.h" +#include "third_party/blink/renderer/core/layout/geometry/logical_rect.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal.h" #include "third_party/blink/renderer/core/layout/ng/ng_layout_test.h" #include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h" @@ -55,9 +55,7 @@ TEST_F(NGPhysicalTextFragmentTest, LocalRect) { )HTML"); auto text_fragments = CollectTextFragmentsInContainer("container"); ASSERT_EQ(2u, text_fragments.size()); - EXPECT_EQ(NGPhysicalOffsetRect({LayoutUnit(20), LayoutUnit(0)}, - {LayoutUnit(20), LayoutUnit(10)}), - text_fragments[1]->LocalRect(8, 10)); + EXPECT_EQ(PhysicalRect(20, 0, 20, 10), text_fragments[1]->LocalRect(8, 10)); } TEST_F(NGPhysicalTextFragmentTest, LocalRectRTL) { @@ -78,8 +76,7 @@ TEST_F(NGPhysicalTextFragmentTest, LocalRectRTL) { // The 2nd line starts at 12, because the div has a bidi-control. EXPECT_EQ(12u, text_fragments[1]->StartOffset()); // TODO(layout-dev): Investigate whether this is correct. - // EXPECT_EQ(NGPhysicalOffsetRect({LayoutUnit(50), LayoutUnit(0)}, - // {LayoutUnit(20), LayoutUnit(10)}), + // EXPECT_EQ(PhysicalRect(50, 0, 20, 10), // text_fragments[1]->LocalRect(14, 16)); } @@ -97,9 +94,7 @@ TEST_F(NGPhysicalTextFragmentTest, LocalRectVLR) { )HTML"); auto text_fragments = CollectTextFragmentsInContainer("container"); ASSERT_EQ(2u, text_fragments.size()); - EXPECT_EQ(NGPhysicalOffsetRect({LayoutUnit(0), LayoutUnit(20)}, - {LayoutUnit(10), LayoutUnit(20)}), - text_fragments[1]->LocalRect(8, 10)); + EXPECT_EQ(PhysicalRect(0, 20, 10, 20), text_fragments[1]->LocalRect(8, 10)); } TEST_F(NGPhysicalTextFragmentTest, LocalRectVRL) { @@ -116,9 +111,7 @@ TEST_F(NGPhysicalTextFragmentTest, LocalRectVRL) { )HTML"); auto text_fragments = CollectTextFragmentsInContainer("container"); ASSERT_EQ(2u, text_fragments.size()); - EXPECT_EQ(NGPhysicalOffsetRect({LayoutUnit(0), LayoutUnit(20)}, - {LayoutUnit(10), LayoutUnit(20)}), - text_fragments[1]->LocalRect(8, 10)); + EXPECT_EQ(PhysicalRect(0, 20, 10, 20), text_fragments[1]->LocalRect(8, 10)); } TEST_F(NGPhysicalTextFragmentTest, NormalTextIsNotAnonymousText) { @@ -128,7 +121,7 @@ TEST_F(NGPhysicalTextFragmentTest, NormalTextIsNotAnonymousText) { ASSERT_EQ(1u, text_fragments.size()); const NGPhysicalTextFragment& text = *text_fragments[0]; - EXPECT_FALSE(text.IsAnonymousText()); + EXPECT_FALSE(text.IsGeneratedText()); } TEST_F(NGPhysicalTextFragmentTest, FirstLetterIsNotAnonymousText) { @@ -141,8 +134,8 @@ TEST_F(NGPhysicalTextFragmentTest, FirstLetterIsNotAnonymousText) { const NGPhysicalTextFragment& first_letter = *text_fragments[0]; const NGPhysicalTextFragment& remaining_text = *text_fragments[1]; - EXPECT_FALSE(first_letter.IsAnonymousText()); - EXPECT_FALSE(remaining_text.IsAnonymousText()); + EXPECT_FALSE(first_letter.IsGeneratedText()); + EXPECT_FALSE(remaining_text.IsGeneratedText()); } TEST_F(NGPhysicalTextFragmentTest, BeforeAndAfterAreAnonymousText) { @@ -156,9 +149,9 @@ TEST_F(NGPhysicalTextFragmentTest, BeforeAndAfterAreAnonymousText) { const NGPhysicalTextFragment& before = *text_fragments[0]; const NGPhysicalTextFragment& text = *text_fragments[1]; const NGPhysicalTextFragment& after = *text_fragments[2]; - EXPECT_TRUE(before.IsAnonymousText()); - EXPECT_FALSE(text.IsAnonymousText()); - EXPECT_TRUE(after.IsAnonymousText()); + EXPECT_TRUE(before.IsGeneratedText()); + EXPECT_FALSE(text.IsGeneratedText()); + EXPECT_TRUE(after.IsGeneratedText()); } TEST_F(NGPhysicalTextFragmentTest, Ellipsis) { @@ -181,15 +174,13 @@ TEST_F(NGPhysicalTextFragmentTest, Ellipsis) { const NGPhysicalTextFragment& ellipsis = *text_fragments[1]; EXPECT_EQ(NGPhysicalTextFragment::kNormalText, abcdef.TextType()); EXPECT_FALSE(abcdef.IsGeneratedText()); - EXPECT_FALSE(abcdef.IsAnonymousText()); EXPECT_EQ(u8"abc", GetText(abcdef)); EXPECT_EQ(NGPhysicalTextFragment::kGeneratedText, ellipsis.TextType()); EXPECT_TRUE(ellipsis.IsGeneratedText()); - EXPECT_TRUE(ellipsis.IsAnonymousText()); EXPECT_EQ(u8"\u2026", GetText(ellipsis)); } -TEST_F(NGPhysicalTextFragmentTest, ListMarkerIsAnonymousText) { +TEST_F(NGPhysicalTextFragmentTest, ListMarkerIsGeneratedText) { SetBodyInnerHTML( "<ol style='list-style-position:inside'>" "<li id=list>text</li>" @@ -200,8 +191,8 @@ TEST_F(NGPhysicalTextFragmentTest, ListMarkerIsAnonymousText) { const NGPhysicalTextFragment& marker = *text_fragments[0]; const NGPhysicalTextFragment& text = *text_fragments[1]; - EXPECT_TRUE(marker.IsAnonymousText()); - EXPECT_FALSE(text.IsAnonymousText()); + EXPECT_TRUE(marker.IsGeneratedText()); + EXPECT_FALSE(text.IsGeneratedText()); } TEST_F(NGPhysicalTextFragmentTest, SoftHyphen) { @@ -222,17 +213,14 @@ TEST_F(NGPhysicalTextFragmentTest, SoftHyphen) { 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) { @@ -244,9 +232,9 @@ TEST_F(NGPhysicalTextFragmentTest, QuotationMarksAreAnonymousText) { const NGPhysicalTextFragment& open_quote = *text_fragments[0]; const NGPhysicalTextFragment& text = *text_fragments[1]; const NGPhysicalTextFragment& closed_quote = *text_fragments[2]; - EXPECT_TRUE(open_quote.IsAnonymousText()); - EXPECT_FALSE(text.IsAnonymousText()); - EXPECT_TRUE(closed_quote.IsAnonymousText()); + EXPECT_TRUE(open_quote.IsGeneratedText()); + EXPECT_FALSE(text.IsGeneratedText()); + EXPECT_TRUE(closed_quote.IsGeneratedText()); } TEST_F(NGPhysicalTextFragmentTest, TextOffsetForPointForTabulation) { 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 07ee69f28b1..65b78d5bd1b 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 @@ -4,6 +4,7 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.h" +#include "third_party/blink/renderer/core/layout/layout_text_fragment.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/core/layout/ng/inline/ng_line_height_metrics.h" @@ -58,6 +59,21 @@ void NGTextFragmentBuilder::SetText( end_effect_ = NGTextEndEffect::kNone; } +bool NGTextFragmentBuilder::IsGeneratedText() const { + if (UNLIKELY(style_variant_ == NGStyleVariant::kEllipsis || + text_type_ == NGPhysicalTextFragment::kGeneratedText)) + return true; + + DCHECK(layout_object_); + if (const auto* layout_text_fragment = + ToLayoutTextFragmentOrNull(layout_object_)) { + return !layout_text_fragment->AssociatedTextNode(); + } + + const Node* node = layout_object_->GetNode(); + return !node || node->IsPseudoElement(); +} + scoped_refptr<const NGPhysicalTextFragment> NGTextFragmentBuilder::ToTextFragment() { scoped_refptr<const NGPhysicalTextFragment> fragment = 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 bf000858f53..b219736b393 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 @@ -5,7 +5,7 @@ #ifndef NGTextFragmentBuilder_h #define NGTextFragmentBuilder_h -#include "third_party/blink/renderer/core/layout/ng/geometry/ng_logical_size.h" +#include "third_party/blink/renderer/core/layout/geometry/logical_size.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_text_end_effect.h" @@ -43,6 +43,10 @@ class CORE_EXPORT NGTextFragmentBuilder final : public NGFragmentBuilder { scoped_refptr<const NGPhysicalTextFragment> ToTextFragment(); private: + // Returns true if the text is generated (from, e.g., list marker, + // pseudo-element, ...) instead of from a DOM text node. + bool IsGeneratedText() const; + NGInlineNode inline_node_; String text_; unsigned item_index_; |