diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2018-08-24 12:15:48 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2018-08-28 13:30:04 +0000 |
commit | b014812705fc80bff0a5c120dfcef88f349816dc (patch) | |
tree | 25a2e2d9fa285f1add86aa333389a839f81a39ae /chromium/third_party/blink/renderer/core/layout/ng/inline | |
parent | 9f4560b1027ae06fdb497023cdcaf91b8511fa74 (diff) | |
download | qtwebengine-chromium-b014812705fc80bff0a5c120dfcef88f349816dc.tar.gz |
BASELINE: Update Chromium to 68.0.3440.125
Change-Id: I23f19369e01f688e496f5bf179abb521ad73874f
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/third_party/blink/renderer/core/layout/ng/inline')
47 files changed, 2013 insertions, 959 deletions
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 new file mode 100644 index 00000000000..8ea80821037 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/layout_ng_text.h @@ -0,0 +1,64 @@ +// 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_LAYOUT_NG_TEXT_H_ +#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_LAYOUT_NG_TEXT_H_ + +#include "third_party/blink/renderer/core/layout/layout_text.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h" + +namespace blink { + +class NGInlineItem; + +// This overrides the default LayoutText to reference LayoutNGInlineItems +// instead of InlineTextBoxes. +// +// ***** INLINE ITEMS OWNERSHIP ***** +// NGInlineItems in items_ are not owned by LayoutText but are pointers into the +// LayoutNGBlockFlow's items_. Should not be accessed outside of layout. +class CORE_EXPORT LayoutNGText : public LayoutText { + public: + LayoutNGText(Node* node, scoped_refptr<StringImpl> text) + : LayoutText(node, text) {} + + bool IsOfType(LayoutObjectType type) const override { + return type == kLayoutObjectNGText || LayoutText::IsOfType(type); + } + + bool HasValidLayout() const { return valid_ng_items_; } + const Vector<NGInlineItem*>& InlineItems() const { + DCHECK(valid_ng_items_); + return inline_items_; + } + + // Inline items depends on context. It needs to be invalidated not only when + // it was inserted/changed but also it was moved. + void InvalidateInlineItems() { valid_ng_items_ = false; } + + void ClearInlineItems() { + inline_items_.clear(); + valid_ng_items_ = false; + } + + void AddInlineItem(NGInlineItem* item) { + inline_items_.push_back(item); + valid_ng_items_ = true; + } + + protected: + void InsertedIntoTree() override { + valid_ng_items_ = false; + LayoutText::InsertedIntoTree(); + } + + private: + Vector<NGInlineItem*> inline_items_; +}; + +DEFINE_LAYOUT_OBJECT_TYPE_CASTS(LayoutNGText, IsLayoutNGText()); + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_LAYOUT_NG_TEXT_H_ 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 new file mode 100644 index 00000000000..cadd9c38992 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.cc @@ -0,0 +1,313 @@ +// 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_position.h" + +#include "third_party/blink/renderer/core/editing/inline_box_traversal.h" +#include "third_party/blink/renderer/core/editing/position_with_affinity.h" +#include "third_party/blink/renderer/core/editing/text_affinity.h" +#include "third_party/blink/renderer/core/layout/layout_block_flow.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h" +#include "third_party/blink/renderer/core/paint/ng/ng_paint_fragment.h" +#include "third_party/blink/renderer/core/paint/ng/ng_paint_fragment_traversal.h" + +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 +// - A TextAffinity +// +// The calculation iterates all inline fragments in the context, and tries to +// compute an NGCaretPosition using the "caret resolution process" below: +// +// The (offset, affinity) pair is compared against each inline fragment to see +// if the corresponding caret should be placed in the fragment, using the +// |TryResolveCaretPositionInXXX()| functions. These functions may return: +// - Failed, indicating that the caret must not be placed in the fragment; +// - Resolved, indicating that the care should be placed in the fragment, and +// no further search is required. The result NGCaretPosition is returned +// together. +// - FoundCandidate, indicating that the caret may be placed in the fragment; +// however, further search may find a better position. The candidate +// NGCaretPosition is also returned together. + +enum class ResolutionType { kFailed, kFoundCandidate, kResolved }; +struct CaretPositionResolution { + ResolutionType type = ResolutionType::kFailed; + NGCaretPosition caret_position; +}; + +bool CanResolveCaretPositionBeforeFragment(const NGPaintFragment& fragment, + TextAffinity affinity) { + if (affinity == TextAffinity::kDownstream) + return true; + const NGPaintFragment* current_line_paint = fragment.ContainerLineBox(); + const NGPhysicalLineBoxFragment& current_line = + ToNGPhysicalLineBoxFragment(current_line_paint->PhysicalFragment()); + // A fragment after line wrap must be the first logical leaf in its line. + if (&fragment.PhysicalFragment() != current_line.FirstLogicalLeaf()) + return true; + const NGPaintFragment* last_line_paint = + NGPaintFragmentTraversal::PreviousLineOf(*current_line_paint); + return !last_line_paint || + !ToNGPhysicalLineBoxFragment(last_line_paint->PhysicalFragment()) + .HasSoftWrapToNextLine(); +} + +bool CanResolveCaretPositionAfterFragment(const NGPaintFragment& fragment, + TextAffinity affinity) { + if (affinity == TextAffinity::kUpstream) + return true; + const NGPaintFragment* current_line_paint = fragment.ContainerLineBox(); + const NGPhysicalLineBoxFragment& current_line = + ToNGPhysicalLineBoxFragment(current_line_paint->PhysicalFragment()); + // A fragment before line wrap must be the last logical leaf in its line. + if (&fragment.PhysicalFragment() != current_line.LastLogicalLeaf()) + return true; + return !current_line.HasSoftWrapToNextLine(); +} + +// Returns a |kFailed| resolution if |offset| doesn't belong to the text +// fragment. Otherwise, return either |kFoundCandidate| or |kResolved| depending +// on |affinity|. +CaretPositionResolution TryResolveCaretPositionInTextFragment( + const NGPaintFragment& paint_fragment, + unsigned offset, + TextAffinity affinity) { + DCHECK(paint_fragment.PhysicalFragment().IsText()); + const NGPhysicalTextFragment& fragment = + ToNGPhysicalTextFragment(paint_fragment.PhysicalFragment()); + if (fragment.IsAnonymousText()) + return CaretPositionResolution(); + + const NGOffsetMapping& mapping = + *NGOffsetMapping::GetFor(paint_fragment.GetLayoutObject()); + + // A text fragment natually allows caret placement in offset range + // [StartOffset(), EndOffset()], i.e., from before the first character to + // after the last character. + // Besides, leading/trailing bidi control characters are ignored since their + // two sides are considered the same caret position. Hence, if there are n and + // m leading and trailing bidi control characters, then the allowed offset + // range is [StartOffset() - n, EndOffset() + m]. + // Note that we don't ignore other characters that are not in fragments. For + // example, a trailing space of a line is not in any fragment, but its two + // sides are still different caret positions, so we don't ignore it. + if (offset < fragment.StartOffset() && + !mapping.HasBidiControlCharactersOnly(offset, fragment.StartOffset())) + return CaretPositionResolution(); + if (offset > fragment.EndOffset() && + !mapping.HasBidiControlCharactersOnly(fragment.EndOffset(), offset)) + return CaretPositionResolution(); + + offset = std::max(offset, fragment.StartOffset()); + offset = std::min(offset, fragment.EndOffset()); + NGCaretPosition candidate = {&paint_fragment, + NGCaretPositionType::kAtTextOffset, offset}; + + // Offsets in the interior of a fragment can be resolved directly. + if (offset > fragment.StartOffset() && offset < fragment.EndOffset()) + return {ResolutionType::kResolved, candidate}; + + if (offset == fragment.StartOffset() && + CanResolveCaretPositionBeforeFragment(paint_fragment, affinity)) { + return {ResolutionType::kResolved, candidate}; + } + + if (offset == fragment.EndOffset() && !fragment.IsLineBreak() && + CanResolveCaretPositionAfterFragment(paint_fragment, affinity)) { + return {ResolutionType::kResolved, candidate}; + } + + // We may have a better candidate + return {ResolutionType::kFoundCandidate, candidate}; +} + +unsigned GetTextOffsetBefore(const NGPhysicalFragment& fragment) { + // TODO(xiaochengh): Design more straightforward way to get text offset of + // atomic inline box. + DCHECK(fragment.IsAtomicInline()); + const Node* node = fragment.GetNode(); + DCHECK(node); + const Position before_node = Position::BeforeNode(*node); + base::Optional<unsigned> maybe_offset_before = + NGOffsetMapping::GetFor(before_node)->GetTextContentOffset(before_node); + // We should have offset mapping for atomic inline boxes. + DCHECK(maybe_offset_before.has_value()); + return maybe_offset_before.value(); +} + +// Returns a |kFailed| resolution if |offset| doesn't belong to the atomic +// inline box fragment. Otherwise, return either |kFoundCandidate| or +// |kResolved| depending on |affinity|. +CaretPositionResolution TryResolveCaretPositionByBoxFragmentSide( + const NGPaintFragment& fragment, + unsigned offset, + TextAffinity affinity) { + if (!fragment.GetNode()) { + // TODO(xiaochengh): This leads to false negatives for, e.g., RUBY, where an + // anonymous wrapping inline block is created. + return CaretPositionResolution(); + } + + const unsigned offset_before = + GetTextOffsetBefore(fragment.PhysicalFragment()); + const unsigned offset_after = offset_before + 1; + // TODO(xiaochengh): Ignore bidi control characters before & after the box. + if (offset != offset_before && offset != offset_after) + return CaretPositionResolution(); + const NGCaretPositionType position_type = + offset == offset_before ? NGCaretPositionType::kBeforeBox + : NGCaretPositionType::kAfterBox; + NGCaretPosition candidate{&fragment, position_type, base::nullopt}; + + if (offset == offset_before && + CanResolveCaretPositionBeforeFragment(fragment, affinity)) { + return {ResolutionType::kResolved, candidate}; + } + + if (offset == offset_after && + CanResolveCaretPositionAfterFragment(fragment, affinity)) { + return {ResolutionType::kResolved, candidate}; + } + + return {ResolutionType::kFoundCandidate, candidate}; +} + +CaretPositionResolution TryResolveCaretPositionWithFragment( + const NGPaintFragment& paint_fragment, + unsigned offset, + TextAffinity affinity) { + const NGPhysicalFragment& fragment = paint_fragment.PhysicalFragment(); + if (fragment.IsText()) { + return TryResolveCaretPositionInTextFragment(paint_fragment, offset, + affinity); + } + if (fragment.IsBox() && fragment.IsAtomicInline()) { + return TryResolveCaretPositionByBoxFragmentSide(paint_fragment, offset, + affinity); + } + return CaretPositionResolution(); +} + +bool NeedsBidiAdjustment(const NGCaretPosition& caret_position) { + if (caret_position.IsNull()) + return false; + if (caret_position.position_type != NGCaretPositionType::kAtTextOffset) + return true; + DCHECK(caret_position.text_offset.has_value()); + DCHECK(caret_position.fragment->PhysicalFragment().IsText()); + const NGPhysicalTextFragment& text_fragment = + ToNGPhysicalTextFragment(caret_position.fragment->PhysicalFragment()); + DCHECK_GE(*caret_position.text_offset, text_fragment.StartOffset()); + DCHECK_LE(*caret_position.text_offset, text_fragment.EndOffset()); + // Bidi adjustment is needed only for caret positions at bidi boundaries. + // Caret positions in the middle of a text fragment can't be at bidi + // boundaries, and hence, don't need any adjustment. + return *caret_position.text_offset == text_fragment.StartOffset() || + *caret_position.text_offset == text_fragment.EndOffset(); +} + +NGCaretPosition AdjustCaretPositionForBidiText( + const NGCaretPosition& caret_position) { + if (!NeedsBidiAdjustment(caret_position)) + return caret_position; + return BidiAdjustment::AdjustForCaretPositionResolution(caret_position); +} + +} // namespace + +// The main function for compute an NGCaretPosition. See the comments at the top +// of this file for details. +NGCaretPosition ComputeNGCaretPosition(const LayoutBlockFlow& context, + unsigned offset, + TextAffinity affinity) { + const NGPaintFragment* root_fragment = context.PaintFragment(); + DCHECK(root_fragment) << "no paint fragment on layout object " << &context; + + NGCaretPosition candidate; + for (const auto& child : + NGPaintFragmentTraversal::InlineDescendantsOf(*root_fragment)) { + const CaretPositionResolution resolution = + TryResolveCaretPositionWithFragment(*child.fragment, offset, affinity); + + if (resolution.type == ResolutionType::kFailed) + continue; + + // TODO(xiaochengh): Handle caret poisition in empty container (e.g. empty + // line box). + + if (resolution.type == ResolutionType::kResolved) + return AdjustCaretPositionForBidiText(resolution.caret_position); + + DCHECK_EQ(ResolutionType::kFoundCandidate, resolution.type); + // TODO(xiaochengh): We are not sure if we can ever find multiple + // candidates. Handle it once reached. + DCHECK(candidate.IsNull()); + candidate = resolution.caret_position; + } + + return AdjustCaretPositionForBidiText(candidate); +} + +NGCaretPosition ComputeNGCaretPosition(const PositionWithAffinity& position) { + AssertValidPositionForCaretPositionComputation(position); + const LayoutBlockFlow* context = + NGInlineFormattingContextOf(position.GetPosition()); + if (!context) + return NGCaretPosition(); + + const NGOffsetMapping* mapping = NGOffsetMapping::GetFor(context); + DCHECK(mapping); + const base::Optional<unsigned> maybe_offset = + mapping->GetTextContentOffset(position.GetPosition()); + if (!maybe_offset.has_value()) { + // TODO(xiaochengh): Investigate if we reach here. + NOTREACHED(); + return NGCaretPosition(); + } + + const unsigned offset = maybe_offset.value(); + const TextAffinity affinity = position.Affinity(); + return ComputeNGCaretPosition(*context, offset, affinity); +} + +Position NGCaretPosition::ToPositionInDOMTree() const { + if (!fragment) + return Position(); + switch (position_type) { + case NGCaretPositionType::kBeforeBox: + if (!fragment->GetNode()) + return Position(); + return Position::BeforeNode(*fragment->GetNode()); + case NGCaretPositionType::kAfterBox: + if (!fragment->GetNode()) + return Position(); + return Position::AfterNode(*fragment->GetNode()); + case NGCaretPositionType::kAtTextOffset: + DCHECK(text_offset.has_value()); + const NGOffsetMapping* mapping = + NGOffsetMapping::GetFor(fragment->GetLayoutObject()); + return mapping->GetFirstPosition(*text_offset); + } + NOTREACHED(); + return Position(); +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.h new file mode 100644 index 00000000000..e6a963099c6 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.h @@ -0,0 +1,55 @@ +// 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_POSITION_H_ +#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_CARET_POSITION_H_ + +#include "base/optional.h" +#include "third_party/blink/renderer/core/core_export.h" +#include "third_party/blink/renderer/core/editing/forward.h" +#include "third_party/blink/renderer/platform/wtf/allocator.h" + +namespace blink { + +class NGPaintFragment; +class LayoutBlockFlow; + +// An NGCaretPosition indicates a caret position relative to an inline +// NGPaintFragment: +// - When |fragment| is box, |position_type| is either |kBeforeBox| or +// |kAfterBox|, indicating either of the two caret positions by the box sides; +// |text_offset| is |nullopt| in this case. +// - When |fragment| is text, |position_type| is |kAtTextOffset|, and +// |text_offset| is in the text offset range of the fragment. +// +// TODO(xiaochengh): Support "in empty container" caret type + +enum class NGCaretPositionType { kBeforeBox, kAfterBox, kAtTextOffset }; +struct NGCaretPosition { + DISALLOW_NEW_EXCEPT_PLACEMENT_NEW(); + + bool IsNull() const { return !fragment; } + + Position ToPositionInDOMTree() const; + + const NGPaintFragment* fragment = nullptr; // owned by root LayoutNGMixin + NGCaretPositionType position_type; + base::Optional<unsigned> text_offset; +}; + +// Given an inline formatting context, a text offset in the context and a text +// affinity, returns the corresponding NGCaretPosition, or null if not found. +// Note that in many cases, null result indicates that we have reached an +// unexpected case that is not properly handled. +CORE_EXPORT NGCaretPosition ComputeNGCaretPosition(const LayoutBlockFlow&, + unsigned, + TextAffinity); + +// Shorthand of the above when the input is a position instead of a +// (context, offset) pair. +NGCaretPosition ComputeNGCaretPosition(const PositionWithAffinity&); + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_CARET_POSITION_H_ diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_rect_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position_test.cc index 1ed9a35639e..da232c2bf83 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_rect_test.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_position_test.cc @@ -2,8 +2,9 @@ // 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_rect.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.h" +#include "third_party/blink/renderer/core/editing/text_affinity.h" #include "third_party/blink/renderer/core/layout/layout_block_flow.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_offset_mapping.h" @@ -14,9 +15,9 @@ namespace blink { -class NGCaretRectTest : public NGLayoutTest { +class NGCaretPositionTest : public NGLayoutTest { public: - NGCaretRectTest() : NGLayoutTest() {} + NGCaretPositionTest() : NGLayoutTest() {} void SetUp() override { NGLayoutTest::SetUp(); @@ -69,31 +70,31 @@ class NGCaretRectTest : public NGLayoutTest { EXPECT_EQ(caret.text_offset, offset_) << caret.text_offset.value_or(-1); \ } -TEST_F(NGCaretRectTest, CaretPositionInOneLineOfText) { +TEST_F(NGCaretPositionTest, CaretPositionInOneLineOfText) { SetInlineFormattingContext("t", "foo", 3); const Node* text = container_->firstChild(); const NGPhysicalFragment* text_fragment = FragmentOf(text); // Beginning of line TEST_CARET(ComputeNGCaretPosition(0, TextAffinity::kDownstream), - text_fragment, kAtTextOffset, Optional<unsigned>(0)); + text_fragment, kAtTextOffset, base::Optional<unsigned>(0)); TEST_CARET(ComputeNGCaretPosition(0, TextAffinity::kUpstream), text_fragment, - kAtTextOffset, Optional<unsigned>(0)); + kAtTextOffset, base::Optional<unsigned>(0)); // Middle in the line TEST_CARET(ComputeNGCaretPosition(1, TextAffinity::kDownstream), - text_fragment, kAtTextOffset, Optional<unsigned>(1)); + text_fragment, kAtTextOffset, base::Optional<unsigned>(1)); TEST_CARET(ComputeNGCaretPosition(1, TextAffinity::kUpstream), text_fragment, - kAtTextOffset, Optional<unsigned>(1)); + kAtTextOffset, base::Optional<unsigned>(1)); // End of line TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kDownstream), - text_fragment, kAtTextOffset, Optional<unsigned>(3)); + text_fragment, kAtTextOffset, base::Optional<unsigned>(3)); TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kUpstream), text_fragment, - kAtTextOffset, Optional<unsigned>(3)); + kAtTextOffset, base::Optional<unsigned>(3)); } -TEST_F(NGCaretRectTest, CaretPositionAtSoftLineWrap) { +TEST_F(NGCaretPositionTest, CaretPositionAtSoftLineWrap) { SetInlineFormattingContext("t", "foobar", 3); const Node* text = container_->firstChild(); const auto text_fragments = NGInlineFragmentTraversal::SelfFragmentsOf( @@ -102,12 +103,12 @@ TEST_F(NGCaretRectTest, CaretPositionAtSoftLineWrap) { const NGPhysicalFragment* bar_fragment = text_fragments[1].fragment.get(); TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kDownstream), bar_fragment, - kAtTextOffset, Optional<unsigned>(3)); + kAtTextOffset, base::Optional<unsigned>(3)); TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kUpstream), foo_fragment, - kAtTextOffset, Optional<unsigned>(3)); + kAtTextOffset, base::Optional<unsigned>(3)); } -TEST_F(NGCaretRectTest, CaretPositionAtSoftLineWrapWithSpace) { +TEST_F(NGCaretPositionTest, CaretPositionAtSoftLineWrapWithSpace) { SetInlineFormattingContext("t", "foo bar", 3); const Node* text = container_->firstChild(); const auto text_fragments = NGInlineFragmentTraversal::SelfFragmentsOf( @@ -117,18 +118,18 @@ TEST_F(NGCaretRectTest, CaretPositionAtSoftLineWrapWithSpace) { // Before the space TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kDownstream), foo_fragment, - kAtTextOffset, Optional<unsigned>(3)); + kAtTextOffset, base::Optional<unsigned>(3)); TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kUpstream), foo_fragment, - kAtTextOffset, Optional<unsigned>(3)); + kAtTextOffset, base::Optional<unsigned>(3)); // After the space TEST_CARET(ComputeNGCaretPosition(4, TextAffinity::kDownstream), bar_fragment, - kAtTextOffset, Optional<unsigned>(4)); + kAtTextOffset, base::Optional<unsigned>(4)); TEST_CARET(ComputeNGCaretPosition(4, TextAffinity::kUpstream), bar_fragment, - kAtTextOffset, Optional<unsigned>(4)); + kAtTextOffset, base::Optional<unsigned>(4)); } -TEST_F(NGCaretRectTest, CaretPositionAtForcedLineBreak) { +TEST_F(NGCaretPositionTest, CaretPositionAtForcedLineBreak) { SetInlineFormattingContext("t", "foo<br>bar", 3); const Node* foo = container_->firstChild(); const Node* br = foo->nextSibling(); @@ -138,18 +139,18 @@ TEST_F(NGCaretRectTest, CaretPositionAtForcedLineBreak) { // Before the BR TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kDownstream), foo_fragment, - kAtTextOffset, Optional<unsigned>(3)); + kAtTextOffset, base::Optional<unsigned>(3)); TEST_CARET(ComputeNGCaretPosition(3, TextAffinity::kUpstream), foo_fragment, - kAtTextOffset, Optional<unsigned>(3)); + kAtTextOffset, base::Optional<unsigned>(3)); // After the BR TEST_CARET(ComputeNGCaretPosition(4, TextAffinity::kDownstream), bar_fragment, - kAtTextOffset, Optional<unsigned>(4)); + kAtTextOffset, base::Optional<unsigned>(4)); TEST_CARET(ComputeNGCaretPosition(4, TextAffinity::kUpstream), bar_fragment, - kAtTextOffset, Optional<unsigned>(4)); + kAtTextOffset, base::Optional<unsigned>(4)); } -TEST_F(NGCaretRectTest, CaretPositionAtEmptyLine) { +TEST_F(NGCaretPositionTest, CaretPositionAtEmptyLine) { SetInlineFormattingContext("f", "foo<br><br>bar", 3); const Node* foo = container_->firstChild(); const Node* br1 = foo->nextSibling(); @@ -157,30 +158,30 @@ TEST_F(NGCaretRectTest, CaretPositionAtEmptyLine) { const NGPhysicalFragment* br2_fragment = FragmentOf(br2); TEST_CARET(ComputeNGCaretPosition(4, TextAffinity::kDownstream), br2_fragment, - kAtTextOffset, Optional<unsigned>(4)); + kAtTextOffset, base::Optional<unsigned>(4)); TEST_CARET(ComputeNGCaretPosition(4, TextAffinity::kUpstream), br2_fragment, - kAtTextOffset, Optional<unsigned>(4)); + kAtTextOffset, base::Optional<unsigned>(4)); } -TEST_F(NGCaretRectTest, CaretPositionInOneLineOfImage) { +TEST_F(NGCaretPositionTest, CaretPositionInOneLineOfImage) { SetInlineFormattingContext("t", "<img>", 3); const Node* img = container_->firstChild(); const NGPhysicalFragment* img_fragment = FragmentOf(img); // Before the image TEST_CARET(ComputeNGCaretPosition(0, TextAffinity::kDownstream), img_fragment, - kBeforeBox, WTF::nullopt); + kBeforeBox, base::nullopt); TEST_CARET(ComputeNGCaretPosition(0, TextAffinity::kUpstream), img_fragment, - kBeforeBox, WTF::nullopt); + kBeforeBox, base::nullopt); // After the image TEST_CARET(ComputeNGCaretPosition(1, TextAffinity::kDownstream), img_fragment, - kAfterBox, WTF::nullopt); + kAfterBox, base::nullopt); TEST_CARET(ComputeNGCaretPosition(1, TextAffinity::kUpstream), img_fragment, - kAfterBox, WTF::nullopt); + kAfterBox, base::nullopt); } -TEST_F(NGCaretRectTest, CaretPositionAtSoftLineWrapBetweenImages) { +TEST_F(NGCaretPositionTest, CaretPositionAtSoftLineWrapBetweenImages) { SetInlineFormattingContext("t", "<img id=img1><img id=img2>" "<style>img{width: 1em; height: 1em}</style>", @@ -191,12 +192,13 @@ TEST_F(NGCaretRectTest, CaretPositionAtSoftLineWrapBetweenImages) { const NGPhysicalFragment* img2_fragment = FragmentOf(img2); TEST_CARET(ComputeNGCaretPosition(1, TextAffinity::kDownstream), - img2_fragment, kBeforeBox, WTF::nullopt); + img2_fragment, kBeforeBox, base::nullopt); TEST_CARET(ComputeNGCaretPosition(1, TextAffinity::kUpstream), img1_fragment, - kAfterBox, WTF::nullopt); + kAfterBox, base::nullopt); } -TEST_F(NGCaretRectTest, CaretPositionAtSoftLineWrapBetweenMultipleTextNodes) { +TEST_F(NGCaretPositionTest, + CaretPositionAtSoftLineWrapBetweenMultipleTextNodes) { SetInlineFormattingContext("t", "<span>A</span>" "<span>B</span>" @@ -216,12 +218,12 @@ TEST_F(NGCaretRectTest, CaretPositionAtSoftLineWrapBetweenMultipleTextNodes) { mapping.GetTextContentOffset(wrap_position).value(); TEST_CARET(ComputeNGCaretPosition(wrap_offset, TextAffinity::kUpstream), - fragment_c, kAtTextOffset, Optional<unsigned>(wrap_offset)); + fragment_c, kAtTextOffset, base::Optional<unsigned>(wrap_offset)); TEST_CARET(ComputeNGCaretPosition(wrap_offset, TextAffinity::kDownstream), - fragment_d, kAtTextOffset, Optional<unsigned>(wrap_offset)); + fragment_d, kAtTextOffset, base::Optional<unsigned>(wrap_offset)); } -TEST_F(NGCaretRectTest, +TEST_F(NGCaretPositionTest, CaretPositionAtSoftLineWrapBetweenMultipleTextNodesRtl) { SetInlineFormattingContext("t", "<span>A</span>" @@ -242,12 +244,12 @@ TEST_F(NGCaretRectTest, mapping.GetTextContentOffset(wrap_position).value(); TEST_CARET(ComputeNGCaretPosition(wrap_offset, TextAffinity::kUpstream), - fragment_c, kAtTextOffset, Optional<unsigned>(wrap_offset)); + fragment_c, kAtTextOffset, base::Optional<unsigned>(wrap_offset)); TEST_CARET(ComputeNGCaretPosition(wrap_offset, TextAffinity::kDownstream), - fragment_d, kAtTextOffset, Optional<unsigned>(wrap_offset)); + fragment_d, kAtTextOffset, base::Optional<unsigned>(wrap_offset)); } -TEST_F(NGCaretRectTest, CaretPositionAtSoftLineWrapBetweenDeepTextNodes) { +TEST_F(NGCaretPositionTest, CaretPositionAtSoftLineWrapBetweenDeepTextNodes) { SetInlineFormattingContext( "t", "<style>span {border: 1px solid black}</style>" @@ -269,9 +271,9 @@ TEST_F(NGCaretRectTest, CaretPositionAtSoftLineWrapBetweenDeepTextNodes) { mapping.GetTextContentOffset(wrap_position).value(); TEST_CARET(ComputeNGCaretPosition(wrap_offset, TextAffinity::kUpstream), - fragment_c, kAtTextOffset, Optional<unsigned>(wrap_offset)); + fragment_c, kAtTextOffset, base::Optional<unsigned>(wrap_offset)); TEST_CARET(ComputeNGCaretPosition(wrap_offset, TextAffinity::kDownstream), - fragment_d, kAtTextOffset, Optional<unsigned>(wrap_offset)); + fragment_d, kAtTextOffset, base::Optional<unsigned>(wrap_offset)); } } // namespace blink 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 7b009be5ee1..b0e7e25ba52 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 @@ -5,187 +5,18 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_caret_rect.h" #include "third_party/blink/renderer/core/editing/local_caret_rect.h" -#include "third_party/blink/renderer/core/editing/position_with_affinity.h" +#include "third_party/blink/renderer/core/frame/local_frame_view.h" #include "third_party/blink/renderer/core/layout/layout_block_flow.h" -#include "third_party/blink/renderer/core/layout/layout_text_fragment.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_offset_mapping.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_caret_position.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h" -#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/paint/ng/ng_paint_fragment_traversal.h" -#include "third_party/blink/renderer/platform/fonts/character_range.h" namespace blink { namespace { -// ------------------------------------- - -// Caret position calculation and its helpers. - -// The calculation takes the following input: -// - An inline formatting context as a |LayoutBlockFlow| -// - An offset in the |text_content_| string of the above context -// - A TextAffinity -// -// The calculation iterates all inline fragments in the context, and tries to -// compute an NGCaretPosition using the "caret resolution process" below: -// -// The (offset, affinity) pair is compared against each inline fragment to see -// if the corresponding caret should be placed in the fragment, using the -// |TryResolveCaretPositionInXXX()| functions. These functions may return: -// - Failed, indicating that the caret must not be placed in the fragment; -// - Resolved, indicating that the care should be placed in the fragment, and -// no further search is required. The result NGCaretPosition is returned -// together. -// - FoundCandidate, indicating that the caret may be placed in the fragment; -// however, further search may find a better position. The candidate -// NGCaretPosition is also returned together. - -enum class ResolutionType { kFailed, kFoundCandidate, kResolved }; -struct CaretPositionResolution { - ResolutionType type = ResolutionType::kFailed; - NGCaretPosition caret_position; -}; - -bool CanResolveCaretPositionBeforeFragment(const NGPaintFragment& fragment, - TextAffinity affinity) { - if (affinity == TextAffinity::kDownstream) - return true; - const NGPaintFragment* current_line_paint = fragment.ContainerLineBox(); - const NGPhysicalLineBoxFragment& current_line = - ToNGPhysicalLineBoxFragment(current_line_paint->PhysicalFragment()); - // A fragment after line wrap must be the first logical leaf in its line. - if (&fragment.PhysicalFragment() != current_line.FirstLogicalLeaf()) - return true; - const NGPaintFragment* last_line_paint = - NGPaintFragmentTraversal::PreviousLineOf(*current_line_paint); - return !last_line_paint || - !ToNGPhysicalLineBoxFragment(last_line_paint->PhysicalFragment()) - .HasSoftWrapToNextLine(); -} - -bool CanResolveCaretPositionAfterFragment(const NGPaintFragment& fragment, - TextAffinity affinity) { - if (affinity == TextAffinity::kUpstream) - return true; - const NGPaintFragment* current_line_paint = fragment.ContainerLineBox(); - const NGPhysicalLineBoxFragment& current_line = - ToNGPhysicalLineBoxFragment(current_line_paint->PhysicalFragment()); - // A fragment before line wrap must be the last logical leaf in its line. - if (&fragment.PhysicalFragment() != current_line.LastLogicalLeaf()) - return true; - return !current_line.HasSoftWrapToNextLine(); -} - -CaretPositionResolution TryResolveCaretPositionInTextFragment( - const NGPaintFragment& paint_fragment, - unsigned offset, - TextAffinity affinity) { - DCHECK(paint_fragment.PhysicalFragment().IsText()); - const NGPhysicalTextFragment& fragment = - ToNGPhysicalTextFragment(paint_fragment.PhysicalFragment()); - if (fragment.IsAnonymousText()) - return CaretPositionResolution(); - - // [StartOffset(), EndOffset()] is the range allowing caret placement. - // For example, "foo" has 4 offsets allowing caret placement. - if (offset < fragment.StartOffset() || offset > fragment.EndOffset()) { - // TODO(xiaochengh): This may introduce false negatives. Investigate. - return CaretPositionResolution(); - } - NGCaretPosition candidate = {&paint_fragment, - NGCaretPositionType::kAtTextOffset, offset}; - - // Offsets in the interior of a fragment can be resolved directly. - if (offset > fragment.StartOffset() && offset < fragment.EndOffset()) - return {ResolutionType::kResolved, candidate}; - - if (offset == fragment.StartOffset() && - CanResolveCaretPositionBeforeFragment(paint_fragment, affinity)) { - return {ResolutionType::kResolved, candidate}; - } - - if (offset == fragment.EndOffset() && !fragment.IsLineBreak() && - CanResolveCaretPositionAfterFragment(paint_fragment, affinity)) { - return {ResolutionType::kResolved, candidate}; - } - - // We may have a better candidate - return {ResolutionType::kFoundCandidate, candidate}; -} - -unsigned GetTextOffsetBefore(const NGPhysicalFragment& fragment) { - // TODO(xiaochengh): Design more straightforward way to get text offset of - // atomic inline box. - DCHECK(fragment.IsAtomicInline()); - const Node* node = fragment.GetNode(); - DCHECK(node); - const Position before_node = Position::BeforeNode(*node); - Optional<unsigned> maybe_offset_before = - NGOffsetMapping::GetFor(before_node)->GetTextContentOffset(before_node); - // We should have offset mapping for atomic inline boxes. - DCHECK(maybe_offset_before.has_value()); - return maybe_offset_before.value(); -} - -CaretPositionResolution TryResolveCaretPositionByBoxFragmentSide( - const NGPaintFragment& fragment, - unsigned offset, - TextAffinity affinity) { - if (!fragment.GetNode()) { - // TODO(xiaochengh): This leads to false negatives for, e.g., RUBY, where an - // anonymous wrapping inline block is created. - return CaretPositionResolution(); - } - - const unsigned offset_before = - GetTextOffsetBefore(fragment.PhysicalFragment()); - const unsigned offset_after = offset_before + 1; - if (offset != offset_before && offset != offset_after) - return CaretPositionResolution(); - const NGCaretPositionType position_type = - offset == offset_before ? NGCaretPositionType::kBeforeBox - : NGCaretPositionType::kAfterBox; - NGCaretPosition candidate{&fragment, position_type, WTF::nullopt}; - - if (offset == offset_before && - CanResolveCaretPositionBeforeFragment(fragment, affinity)) { - return {ResolutionType::kResolved, candidate}; - } - - if (offset == offset_after && - CanResolveCaretPositionAfterFragment(fragment, affinity)) { - return {ResolutionType::kResolved, candidate}; - } - - return {ResolutionType::kFoundCandidate, candidate}; -} - -CaretPositionResolution TryResolveCaretPositionWithFragment( - const NGPaintFragment& paint_fragment, - unsigned offset, - TextAffinity affinity) { - const NGPhysicalFragment& fragment = paint_fragment.PhysicalFragment(); - if (fragment.IsText()) { - return TryResolveCaretPositionInTextFragment(paint_fragment, offset, - affinity); - } - if (fragment.IsBox() && fragment.IsAtomicInline()) { - return TryResolveCaretPositionByBoxFragmentSide(paint_fragment, offset, - affinity); - } - return CaretPositionResolution(); -} - -// ------------------------------------- - -// Helpers for converting NGCaretPositions to caret rects. - NGPhysicalOffsetRect ComputeLocalCaretRectByBoxSide( - const LayoutBlockFlow& context, const NGPaintFragment& fragment, NGCaretPositionType position_type) { const bool is_horizontal = fragment.Style().IsHorizontalWritingMode(); @@ -203,7 +34,7 @@ NGPhysicalOffsetRect ComputeLocalCaretRectByBoxSide( fragment.GetLayoutObject()->GetDocument().View(); LayoutUnit caret_width = frame_view->CaretWidth(); - const bool is_ltr = fragment.Style().Direction() == TextDirection::kLtr; + const bool is_ltr = IsLtr(fragment.PhysicalFragment().ResolvedDirection()); LayoutUnit caret_left; if (is_ltr != (position_type == NGCaretPositionType::kBeforeBox)) { if (is_horizontal) @@ -223,7 +54,6 @@ NGPhysicalOffsetRect ComputeLocalCaretRectByBoxSide( } NGPhysicalOffsetRect ComputeLocalCaretRectAtTextOffset( - const LayoutBlockFlow& context, const NGPaintFragment& paint_fragment, unsigned offset) { const NGPhysicalTextFragment& fragment = @@ -255,7 +85,8 @@ NGPhysicalOffsetRect ComputeLocalCaretRectAtTextOffset( paint_fragment.InlineOffsetToContainerBox(); NGPhysicalSize caret_size(caret_width, caret_height); - const NGPhysicalBoxFragment& context_fragment = *context.CurrentFragment(); + const NGPaintFragment& context_fragment = + *NGPaintFragment::GetForInlineContainer(fragment.GetLayoutObject()); const NGPaintFragment* line_box = paint_fragment.ContainerLineBox(); const NGPhysicalOffset line_box_offset = line_box->InlineOffsetToContainerBox(); @@ -284,101 +115,48 @@ NGPhysicalOffsetRect ComputeLocalCaretRectAtTextOffset( return NGPhysicalOffsetRect(caret_location, caret_size); } -LocalCaretRect ComputeLocalCaretRect(const LayoutBlockFlow& context, - const NGCaretPosition& caret_position) { +LocalCaretRect ComputeLocalCaretRect(const NGCaretPosition& caret_position) { if (caret_position.IsNull()) return LocalCaretRect(); + const NGPaintFragment& fragment = *caret_position.fragment; + const LayoutObject* layout_object = fragment.GetLayoutObject(); switch (caret_position.position_type) { case NGCaretPositionType::kBeforeBox: case NGCaretPositionType::kAfterBox: { - DCHECK(caret_position.fragment->PhysicalFragment().IsBox()); + DCHECK(fragment.PhysicalFragment().IsBox()); const NGPhysicalOffsetRect fragment_local_rect = - ComputeLocalCaretRectByBoxSide(context, *caret_position.fragment, + ComputeLocalCaretRectByBoxSide(fragment, caret_position.position_type); - return {caret_position.fragment->GetLayoutObject(), - fragment_local_rect.ToLayoutRect()}; + return {layout_object, fragment_local_rect.ToLayoutRect()}; } case NGCaretPositionType::kAtTextOffset: { - DCHECK(caret_position.fragment->PhysicalFragment().IsText()); + DCHECK(fragment.PhysicalFragment().IsText()); DCHECK(caret_position.text_offset.has_value()); const NGPhysicalOffsetRect caret_rect = ComputeLocalCaretRectAtTextOffset( - context, *caret_position.fragment, *caret_position.text_offset); - - return {caret_position.fragment->GetLayoutObject(), - caret_rect.ToLayoutRect()}; + 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->EnclosingNGBlockFlow(); + container->FlipForWritingMode(layout_rect); + } + + return {layout_object, layout_rect}; } } NOTREACHED(); - return {caret_position.fragment->GetLayoutObject(), LayoutRect()}; -} - -// ------------------------------------- - -void AssertValidPositionForCaretRectComputation( - 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 + return {layout_object, LayoutRect()}; } } // namespace -// The main function for compute an NGCaretPosition. See the comments at the top -// of this file for details. -NGCaretPosition ComputeNGCaretPosition(const LayoutBlockFlow& context, - unsigned offset, - TextAffinity affinity) { - const NGPaintFragment* root_fragment = context.PaintFragment(); - DCHECK(root_fragment); - - NGCaretPosition candidate; - for (const auto& child : - NGPaintFragmentTraversal::InlineDescendantsOf(*root_fragment)) { - const CaretPositionResolution resolution = - TryResolveCaretPositionWithFragment(*child.fragment, offset, affinity); - - if (resolution.type == ResolutionType::kFailed) - continue; - - // TODO(xiaochengh): Handle caret poisition in empty container (e.g. empty - // line box). - - if (resolution.type == ResolutionType::kResolved) - return resolution.caret_position; - - DCHECK_EQ(ResolutionType::kFoundCandidate, resolution.type); - // TODO(xiaochengh): We are not sure if we can ever find multiple - // candidates. Handle it once reached. - DCHECK(candidate.IsNull()); - candidate = resolution.caret_position; - } - - return candidate; -} - -LocalCaretRect ComputeNGLocalCaretRect(const LayoutBlockFlow& context, - const PositionWithAffinity& position) { - AssertValidPositionForCaretRectComputation(position); - DCHECK_EQ(&context, NGInlineFormattingContextOf(position.GetPosition())); - const NGOffsetMapping* mapping = NGOffsetMapping::GetFor(&context); - DCHECK(mapping); - const Optional<unsigned> maybe_offset = - mapping->GetTextContentOffset(position.GetPosition()); - if (!maybe_offset.has_value()) { - // TODO(xiaochengh): Investigate if we reach here. - NOTREACHED(); - return LocalCaretRect(); - } - - const unsigned offset = maybe_offset.value(); - const TextAffinity affinity = position.Affinity(); - return ComputeLocalCaretRect( - context, ComputeNGCaretPosition(context, offset, affinity)); +LocalCaretRect ComputeNGLocalCaretRect(const PositionWithAffinity& position) { + return ComputeLocalCaretRect(ComputeNGCaretPosition(position)); } } // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_rect.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_rect.h index 2268a8220c8..4215ef0d6f5 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_rect.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_caret_rect.h @@ -5,53 +5,20 @@ #ifndef NGCaretRect_h #define NGCaretRect_h +#include "third_party/blink/renderer/core/core_export.h" #include "third_party/blink/renderer/core/editing/forward.h" -#include "third_party/blink/renderer/core/layout/ng/geometry/ng_physical_offset_rect.h" -#include "third_party/blink/renderer/platform/wtf/forward.h" -#include "third_party/blink/renderer/platform/wtf/optional.h" namespace blink { // This file provides utility functions for computing caret rect in LayoutNG. -class NGPaintFragment; -class LayoutBlockFlow; struct LocalCaretRect; -// Given an inline formatting context and a position in the context, returns the -// caret rect if a caret should be placed at the position, with the given -// affinity. The caret rect location is local to the given formatting context. -CORE_EXPORT LocalCaretRect ComputeNGLocalCaretRect(const LayoutBlockFlow&, - const PositionWithAffinity&); - -// An NGCaretPosition indicates a caret position relative to an inline -// NGPaintFragment: -// - When |fragment| is box, |position_type| is either |kBeforeBox| or -// |kAfterBox|, indicating either of the two caret positions by the box sides; -// |text_offset| is |nullopt| in this case. -// - When |fragment| is text, |position_type| is |kAtTextOffset|, and -// |text_offset| is in the text offset range of the fragment. -// -// TODO(xiaochengh): Support "in empty container" caret type - -enum class NGCaretPositionType { kBeforeBox, kAfterBox, kAtTextOffset }; -struct NGCaretPosition { - DISALLOW_NEW_EXCEPT_PLACEMENT_NEW(); - - bool IsNull() const { return !fragment; } - - const NGPaintFragment* fragment = nullptr; // owned by root LayoutNGMixin - NGCaretPositionType position_type; - Optional<unsigned> text_offset; -}; - -// Given an inline formatting context, a text offset in the context and a text -// affinity, returns the corresponding NGCaretPosition, or null if not found. -// Note that in many cases, null result indicates that we have reached an -// unexpected case that is not properly handled. -CORE_EXPORT NGCaretPosition ComputeNGCaretPosition(const LayoutBlockFlow&, - unsigned, - TextAffinity); +// Given a position with affinity, returns the caret rect if the position is +// laid out with LayoutNG, and a caret can be placed at the position with the +// given affinity. The caret rect location is local to the containing inline +// formatting context. +CORE_EXPORT LocalCaretRect ComputeNGLocalCaretRect(const PositionWithAffinity&); } // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.cc index 3cffb0aab2c..fca16e8c347 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 @@ -274,10 +274,13 @@ void NGInlineLayoutStateStack::AddBoxFragmentPlaceholder( // Do not defer creating a box fragment if this is an empty inline box. // An empty box fragment is still flat that we do not have to defer. // Also, placeholders cannot be reordred if empty. - scoped_refptr<NGLayoutResult> layout_result = - box_data.CreateBoxFragment(line_box); offset.inline_offset += box_data.margin_line_left; - line_box->AddChild(layout_result, offset, box_data.size.inline_size, 0); + LayoutUnit advance = box_data.margin_border_padding_line_left + + box_data.margin_border_padding_line_right; + box_data.size.inline_size = + advance - box_data.margin_line_left - box_data.margin_line_right; + line_box->AddChild(box_data.CreateBoxFragment(line_box), offset, advance, + 0); box_data_list_.pop_back(); } } @@ -365,55 +368,67 @@ LayoutUnit NGInlineLayoutStateStack::ComputeInlinePositions( if (box_data_list_.IsEmpty()) return position; - // Create box fragments. + // Compute inline positions of inline boxes. for (auto& box_data : box_data_list_) { unsigned start = box_data.fragment_start; unsigned end = box_data.fragment_end; DCHECK_GT(end, start); NGLineBoxFragmentBuilder::Child& start_child = (*line_box)[start]; + // Clamping left offset is not defined, match to the existing behavior. LayoutUnit line_left_offset = start_child.offset.inline_offset.ClampNegativeToZero(); LayoutUnit line_right_offset = end < line_box->size() ? (*line_box)[end].offset.inline_offset : position; - box_data.offset.inline_offset = line_left_offset; - box_data.size.inline_size = line_right_offset - line_left_offset; + box_data.offset.inline_offset = + line_left_offset + box_data.margin_line_left; + box_data.size.inline_size = + line_right_offset - line_left_offset + + box_data.margin_border_padding_line_left - box_data.margin_line_left + + box_data.margin_border_padding_line_right - box_data.margin_line_right; + + // Adjust child offsets for margin/border/padding. + if (box_data.margin_border_padding_line_left) { + line_box->MoveInInlineDirection(box_data.margin_border_padding_line_left, + start, line_box->size()); + position += box_data.margin_border_padding_line_left; + } + + if (box_data.margin_border_padding_line_right) { + line_box->MoveInInlineDirection(box_data.margin_border_padding_line_right, + end, line_box->size()); + position += box_data.margin_border_padding_line_right; + } + } + + return position; +} + +void NGInlineLayoutStateStack::CreateBoxFragments( + NGLineBoxFragmentBuilder::ChildList* line_box) { + DCHECK(!box_data_list_.IsEmpty()); + + for (auto& box_data : box_data_list_) { + unsigned start = box_data.fragment_start; + unsigned end = box_data.fragment_end; + DCHECK_GT(end, start); + NGLineBoxFragmentBuilder::Child& start_child = (*line_box)[start]; scoped_refptr<NGLayoutResult> box_fragment = box_data.CreateBoxFragment(line_box); - NGLogicalOffset offset(line_left_offset + box_data.margin_line_left, - box_data.offset.block_offset); if (!start_child.HasFragment()) { start_child.layout_result = std::move(box_fragment); - start_child.offset = offset; + start_child.offset = box_data.offset; } else { // In most cases, |start_child| is moved to the children of the box, and // is empty. It's not empty when it's out-of-flow. Insert in such case. - line_box->InsertChild(start, std::move(box_fragment), offset, + line_box->InsertChild(start, std::move(box_fragment), box_data.offset, LayoutUnit(), 0); } - - // Out-of-flow fragments are left in (start + 1, end). Move them by the left - // margin/border/padding. - if (box_data.margin_border_padding_line_left) { - line_box->MoveInInlineDirection(box_data.margin_border_padding_line_left, - start + 1, end); - } - // Move the rest of children by the inline size the box consumes. - LayoutUnit margin_border_padding = - box_data.margin_border_padding_line_left + - box_data.margin_border_padding_line_right; - if (margin_border_padding) { - line_box->MoveInInlineDirection(margin_border_padding, end, - line_box->size()); - position += margin_border_padding; - } } box_data_list_.clear(); - - return position; } scoped_refptr<NGLayoutResult> @@ -427,17 +442,12 @@ NGInlineLayoutStateStack::BoxData::CreateBoxFragment( NGFragmentBuilder box(item->GetLayoutObject(), &style, style.GetWritingMode(), TextDirection::kLtr); box.SetBoxType(NGPhysicalFragment::kInlineBox); + box.SetStyleVariant(item->StyleVariant()); // Inline boxes have block start/end borders, even when its containing block // 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}); - LayoutUnit border_padding_line_left = - margin_border_padding_line_left - margin_line_left; - LayoutUnit border_padding_line_right = - margin_border_padding_line_right - margin_line_right; - offset.inline_offset -= border_padding_line_left; - size.inline_size += border_padding_line_left + border_padding_line_right; box.SetInlineSize(size.inline_size.ClampNegativeToZero()); box.SetBlockSize(size.block_size); box.SetPadding(padding); @@ -462,7 +472,9 @@ NGInlineLayoutStateStack::ApplyBaselineShift( NGInlineBoxState* box, NGLineBoxFragmentBuilder::ChildList* line_box, FontBaseline baseline_type) { - // Compute descendants that depend on the layout size of this box if any. + // Some 'vertical-align' values require the size of their parents. Align all + // such descendant boxes that require the size of this box; they are queued in + // |pending_descendants|. LayoutUnit baseline_shift; if (!box->pending_descendants.IsEmpty()) { for (auto& child : box->pending_descendants) { @@ -514,9 +526,15 @@ NGInlineLayoutStateStack::ApplyBaselineShift( if (vertical_align == EVerticalAlign::kBaseline) return kPositionNotPending; - // 'vertical-align' aplies only to inline-level elements. + // 'vertical-align' aligns boxes relative to themselves, to their parent + // boxes, or to the line box, depends on the value. + // Because |box| is an item in |stack_|, |box[-1]| is its parent box. + // If this box doesn't have a parent; i.e., this box is a line box, + // 'vertical-align' has no effect. + DCHECK(box >= stack_.begin() && box < stack_.end()); if (box == stack_.begin()) return kPositionNotPending; + NGInlineBoxState& parent_box = box[-1]; // Check if there are any fragments to move. unsigned fragment_end = line_box->size(); @@ -525,10 +543,10 @@ NGInlineLayoutStateStack::ApplyBaselineShift( switch (vertical_align) { case EVerticalAlign::kSub: - baseline_shift = style.ComputedFontSizeAsFixed() / 5 + 1; + baseline_shift = parent_box.style->ComputedFontSizeAsFixed() / 5 + 1; break; case EVerticalAlign::kSuper: - baseline_shift = -(style.ComputedFontSizeAsFixed() / 3 + 1); + baseline_shift = -(parent_box.style->ComputedFontSizeAsFixed() / 3 + 1); break; case EVerticalAlign::kLength: { // 'Percentages: refer to the 'line-height' of the element itself'. @@ -542,9 +560,10 @@ NGInlineLayoutStateStack::ApplyBaselineShift( } case EVerticalAlign::kMiddle: baseline_shift = (box->metrics.ascent - box->metrics.descent) / 2; - if (const SimpleFontData* font_data = style.GetFont().PrimaryFont()) { + if (const SimpleFontData* parent_font_data = + parent_box.style->GetFont().PrimaryFont()) { baseline_shift -= LayoutUnit::FromFloatRound( - font_data->GetFontMetrics().XHeight() / 2); + parent_font_data->GetFontMetrics().XHeight() / 2); } break; case EVerticalAlign::kBaselineMiddle: @@ -558,8 +577,7 @@ NGInlineLayoutStateStack::ApplyBaselineShift( return kPositionPending; default: // Other values require the layout size of the parent box. - SECURITY_CHECK(box != stack_.begin()); - box[-1].pending_descendants.push_back(NGPendingPositions{ + parent_box.pending_descendants.push_back(NGPendingPositions{ box->fragment_start, fragment_end, box->metrics, vertical_align}); return kPositionPending; } 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 68fd6c23673..626ad129d87 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,6 @@ #ifndef NGInlineBoxState_h #define NGInlineBoxState_h -#include "third_party/blink/renderer/core/layout/ng/geometry/ng_border_edges.h" #include "third_party/blink/renderer/core/layout/ng/geometry/ng_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" @@ -140,10 +139,13 @@ class CORE_EXPORT NGInlineLayoutStateStack { // reordering. void UpdateAfterReorder(NGLineBoxFragmentBuilder::ChildList*); - // Compute inline positions of fragments. Also creates box fragments if - // needed. + // Compute inline positions of fragments and boxes. LayoutUnit ComputeInlinePositions(NGLineBoxFragmentBuilder::ChildList*); + // Create box fragments. This function turns a flat list of children into + // a box tree. + void CreateBoxFragments(NGLineBoxFragmentBuilder::ChildList*); + private: // End of a box state, either explicitly by close tag, or implicitly at the // end of a line. 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 870af1c1802..353acbabc68 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 @@ -24,6 +24,9 @@ void CollectInlineFragments(const NGPhysicalContainerFragment& container, NGPhysicalOffset offset_to_container_box, Filter& filter, Vector<Result, inline_capacity>* results) { + DCHECK(container.IsInline() || container.IsLineBox() || + (container.IsBlockFlow() && + ToNGPhysicalBoxFragment(container).ChildrenInline())); for (const auto& child : container.Children()) { NGPhysicalOffset child_offset = child->Offset() + offset_to_container_box; diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal_test.cc index fe68677cdc2..40bf8c8431a 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal_test.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_fragment_traversal_test.cc @@ -64,7 +64,7 @@ TEST_F(NGInlineFragmentTraversalTest, DescendantsOf) { "<div id=t>foo<b id=b>bar</b><br>baz</div>"); const auto descendants = NGInlineFragmentTraversal::DescendantsOf(GetRootFragmentById("t")); - auto iter = descendants.begin(); + auto* iter = descendants.begin(); EXPECT_NEXT_LINE_BOX(iter); EXPECT_NEXT_TEXT(iter, "foo"); @@ -82,7 +82,7 @@ TEST_F(NGInlineFragmentTraversalTest, InclusiveDescendantsOf) { "<div id=t>foo<b id=b>bar</b><br>baz</div>"); auto descendants = NGInlineFragmentTraversal::InclusiveDescendantsOf( GetRootFragmentById("t")); - auto iter = descendants.begin(); + auto* iter = descendants.begin(); EXPECT_NEXT_BOX(iter, "t"); EXPECT_NEXT_LINE_BOX(iter); @@ -102,7 +102,7 @@ TEST_F(NGInlineFragmentTraversalTest, SelfFragmentsOf) { const auto descendants = NGInlineFragmentTraversal::SelfFragmentsOf( GetRootFragmentById("t"), GetLayoutObjectByElementId("filter")); - auto iter = descendants.begin(); + auto* iter = descendants.begin(); // <b> generates two box fragments since its content is in two lines. EXPECT_NEXT_BOX(iter, "filter"); @@ -123,7 +123,7 @@ TEST_F(NGInlineFragmentTraversalTest, AncestorsOf) { const NGPhysicalFragment& target = GetFragmentOfNode(root, GetElementById("target")->firstChild()); auto ancestors = NGInlineFragmentTraversal::AncestorsOf(root, target); - auto iter = ancestors.begin(); + auto* iter = ancestors.begin(); EXPECT_NEXT_BOX(iter, "target"); EXPECT_NEXT_BOX(iter, "i"); @@ -143,7 +143,7 @@ TEST_F(NGInlineFragmentTraversalTest, InclusiveAncestorsOf) { GetFragmentOfNode(root, GetElementById("target")->firstChild()); auto ancestors = NGInlineFragmentTraversal::InclusiveAncestorsOf(root, target); - auto iter = ancestors.begin(); + auto* iter = ancestors.begin(); EXPECT_NEXT_TEXT(iter, "foo"); EXPECT_NEXT_BOX(iter, "target"); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.cc index 42fd483de3d..8e5a6288ddf 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.cc @@ -7,7 +7,6 @@ #include "third_party/blink/renderer/core/layout/layout_inline.h" #include "third_party/blink/renderer/core/layout/layout_object.h" #include "third_party/blink/renderer/core/style/computed_style.h" -#include "third_party/blink/renderer/platform/fonts/character_range.h" #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_buffer.h" namespace blink { @@ -44,7 +43,8 @@ NGInlineItem::NGInlineItem(NGInlineItemType type, unsigned start, unsigned end, const ComputedStyle* style, - LayoutObject* layout_object) + LayoutObject* layout_object, + bool end_may_collapse) : start_offset_(start), end_offset_(end), script_(USCRIPT_INVALID_CODE), @@ -56,11 +56,33 @@ NGInlineItem::NGInlineItem(NGInlineItemType type, is_empty_item_(false), should_create_box_fragment_(false), style_variant_(static_cast<unsigned>(NGStyleVariant::kStandard)), - end_collapse_type_(kNotCollapsible) { + end_collapse_type_(kNotCollapsible), + end_may_collapse_(end_may_collapse) { DCHECK_GE(end, start); ComputeBoxProperties(); } +NGInlineItem::NGInlineItem(const NGInlineItem& other, + unsigned start, + unsigned end, + scoped_refptr<const ShapeResult> shape_result) + : start_offset_(start), + end_offset_(end), + script_(other.script_), + shape_result_(shape_result), + style_(other.style_), + layout_object_(other.layout_object_), + type_(other.type_), + bidi_level_(other.bidi_level_), + shape_options_(other.shape_options_), + is_empty_item_(other.is_empty_item_), + should_create_box_fragment_(other.should_create_box_fragment_), + style_variant_(other.style_variant_), + end_collapse_type_(other.end_collapse_type_), + end_may_collapse_(other.end_may_collapse_) { + DCHECK_GE(end, start); +} + NGInlineItem::~NGInlineItem() = default; void NGInlineItem::ComputeBoxProperties() { @@ -82,7 +104,7 @@ void NGInlineItem::ComputeBoxProperties() { should_create_box_fragment_ = ToLayoutBoxModelObject(layout_object_)->HasSelfPaintingLayer() || style_->HasOutline() || style_->CanContainAbsolutePositionObjects() || - style_->CanContainFixedPositionObjects(); + style_->CanContainFixedPositionObjects(false); } return; } @@ -99,6 +121,13 @@ const char* NGInlineItem::NGInlineItemTypeToString(int val) const { return kNGInlineItemTypeStrings[val]; } +void NGInlineItem::SetBidiLevel(UBiDiLevel level) { + // Invalidate ShapeResult because it depends on the resolved direction. + if (DirectionFromLevel(level) != DirectionFromLevel(bidi_level_)) + shape_result_ = nullptr; + bidi_level_ = level; +} + // Set bidi level to a list of NGInlineItem from |index| to the item that ends // with |end_offset|. // If |end_offset| is mid of an item, the item is split to ensure each item has @@ -113,14 +142,14 @@ unsigned NGInlineItem::SetBidiLevel(Vector<NGInlineItem>& items, unsigned end_offset, UBiDiLevel level) { for (; items[index].end_offset_ < end_offset; index++) - items[index].bidi_level_ = level; - items[index].bidi_level_ = level; + items[index].SetBidiLevel(level); + items[index].SetBidiLevel(level); if (items[index].end_offset_ == end_offset) { // Let close items have the same bidi-level as the previous item. while (index + 1 < items.size() && items[index + 1].Type() == NGInlineItem::kCloseTag) { - items[++index].bidi_level_ = level; + items[++index].SetBidiLevel(level); } } else { Split(items, index, end_offset); @@ -152,6 +181,7 @@ void NGInlineItem::Split(Vector<NGInlineItem>& items, unsigned offset) { DCHECK_GT(offset, items[index].start_offset_); DCHECK_LT(offset, items[index].end_offset_); + items[index].shape_result_ = nullptr; items.insert(index + 1, items[index]); items[index].end_offset_ = offset; items[index + 1].start_offset_ = offset; @@ -161,11 +191,15 @@ void NGInlineItem::SetOffset(unsigned start, unsigned end) { DCHECK_GE(end, start); start_offset_ = start; end_offset_ = end; + // Any modification to the offset will invalidate the shape result. + shape_result_ = nullptr; } void NGInlineItem::SetEndOffset(unsigned end_offset) { DCHECK_GE(end_offset, start_offset_); end_offset_ = end_offset; + // Any modification to the offset will invalidate the shape result. + shape_result_ = nullptr; } bool NGInlineItem::HasStartEdge() const { @@ -181,14 +215,4 @@ bool NGInlineItem::HasEndEdge() const { !ToLayoutInline(GetLayoutObject())->Continuation(); } -NGInlineItemRange::NGInlineItemRange(Vector<NGInlineItem>* items, - unsigned start_index, - unsigned end_index) - : start_item_(&(*items)[start_index]), - size_(end_index - start_index), - start_index_(start_index) { - CHECK_LE(start_index, end_index); - CHECK_LE(end_index, items->size()); -} - } // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h index d9b183b3e0b..1b40a3a53d7 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h @@ -6,19 +6,17 @@ #define NGInlineItem_h #include "third_party/blink/renderer/core/core_export.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h" #include "third_party/blink/renderer/core/layout/ng/ng_style_variant.h" -#include "third_party/blink/renderer/platform/fonts/font_fallback_priority.h" +#include "third_party/blink/renderer/core/style/computed_style.h" #include "third_party/blink/renderer/platform/fonts/shaping/shape_result.h" -#include "third_party/blink/renderer/platform/fonts/simple_font_data.h" -#include "third_party/blink/renderer/platform/layout_unit.h" #include "third_party/blink/renderer/platform/text/text_direction.h" -#include "third_party/blink/renderer/platform/wtf/hash_set.h" #include <unicode/ubidi.h> +#include <unicode/uscript.h> namespace blink { -class ComputedStyle; class LayoutObject; // Class representing a single text node or styled inline element with text @@ -66,9 +64,16 @@ class CORE_EXPORT NGInlineItem { unsigned start, unsigned end, const ComputedStyle* style = nullptr, - LayoutObject* layout_object = nullptr); + LayoutObject* layout_object = nullptr, + bool end_may_collapse = false); ~NGInlineItem(); + // Copy constructor adjusting start/end and shape results. + NGInlineItem(const NGInlineItem&, + unsigned adjusted_start, + unsigned adjusted_end, + scoped_refptr<const ShapeResult>); + NGInlineItemType Type() const { return static_cast<NGInlineItemType>(type_); } const char* NGInlineItemTypeToString(int val) const; @@ -118,7 +123,13 @@ class CORE_EXPORT NGInlineItem { } void SetEndCollapseType(NGCollapseType type) { end_collapse_type_ = type; } + // Whether the item may be affected by whitespace collapsing. Unlike the + // EndCollapseType() method this returns true even if a trailing space has + // been removed. + bool EndMayCollapse() const { return end_may_collapse_; } + static void Split(Vector<NGInlineItem>&, unsigned index, unsigned offset); + void SetBidiLevel(UBiDiLevel); static unsigned SetBidiLevel(Vector<NGInlineItem>&, unsigned index, unsigned end_offset, @@ -146,6 +157,7 @@ class CORE_EXPORT NGInlineItem { unsigned should_create_box_fragment_ : 1; unsigned style_variant_ : 2; unsigned end_collapse_type_ : 2; // NGCollapseType + unsigned end_may_collapse_ : 1; friend class NGInlineNode; }; @@ -159,41 +171,23 @@ inline void NGInlineItem::AssertEndOffset(unsigned offset) const { DCHECK_LE(offset, end_offset_); } -// A vector-like object that points to a subset of an array of |NGInlineItem|. -// The source vector must keep alive and must not resize while this object -// is alive. -class NGInlineItemRange { - STACK_ALLOCATED(); - - public: - NGInlineItemRange(Vector<NGInlineItem>*, - unsigned start_index, - unsigned end_index); +// Represents a text content with a list of NGInlineItem. A node may have an +// additional NGInlineItemsData for ::first-line pseudo element. +struct CORE_EXPORT NGInlineItemsData { + // Text content for all inline items represented by a single NGInlineNode. + // Encoded either as UTF-16 or latin-1 depending on the content. + String text_content; + Vector<NGInlineItem> items; - unsigned StartIndex() const { return start_index_; } - unsigned EndIndex() const { return start_index_ + size_; } - unsigned Size() const { return size_; } + // The DOM to text content offset mapping of this inline node. + std::unique_ptr<NGOffsetMapping> offset_mapping; - NGInlineItem& operator[](unsigned index) { - CHECK_LT(index, size_); - return start_item_[index]; + void AssertOffset(unsigned index, unsigned offset) const { + items[index].AssertOffset(offset); } - const NGInlineItem& operator[](unsigned index) const { - CHECK_LT(index, size_); - return start_item_[index]; + void AssertEndOffset(unsigned index, unsigned offset) const { + items[index].AssertEndOffset(offset); } - - using iterator = NGInlineItem*; - using const_iterator = const NGInlineItem*; - iterator begin() { return start_item_; } - iterator end() { return start_item_ + size_; } - const_iterator begin() const { return start_item_; } - const_iterator end() const { return start_item_ + size_; } - - private: - NGInlineItem* start_item_; - unsigned size_; - unsigned start_index_; }; } // namespace blink 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 74e3e3ca598..43cdd7b803a 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 @@ -19,14 +19,14 @@ NGInlineItemResult::NGInlineItemResult(const NGInlineItem* item, : item(item), item_index(index), start_offset(start), end_offset(end) {} void NGLineInfo::SetLineStyle(const NGInlineNode& node, + const NGInlineItemsData& items_data, const NGConstraintSpace& constraint_space, bool is_first_line, + bool use_first_line_style, bool is_after_forced_break) { - LayoutObject* layout_object = node.GetLayoutObject(); - use_first_line_style_ = - is_first_line && - layout_object->GetDocument().GetStyleEngine().UsesFirstLineRules(); - line_style_ = layout_object->Style(use_first_line_style_); + use_first_line_style_ = use_first_line_style; + items_data_ = &items_data; + line_style_ = node.GetLayoutObject()->Style(use_first_line_style_); if (line_style_->ShouldUseTextIndent(is_first_line, is_after_forced_break)) { // 'text-indent' applies to block container, and percentage is of its 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 401e52a0a54..514ae11066d 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 @@ -19,6 +19,8 @@ class NGConstraintSpace; class NGInlineItem; class NGInlineNode; +struct NGInlineItemsData; + // The result of measuring NGInlineItem. // // This is a transient context object only while building line boxes. @@ -106,14 +108,21 @@ class CORE_EXPORT NGLineInfo { NGLineInfo() = default; explicit NGLineInfo(size_t capacity) : results_(capacity) {} + const NGInlineItemsData& ItemsData() const { + DCHECK(items_data_); + return *items_data_; + } + // The style to use for the line. const ComputedStyle& LineStyle() const { DCHECK(line_style_); return *line_style_; } void SetLineStyle(const NGInlineNode&, + const NGInlineItemsData&, const NGConstraintSpace&, - bool is_first_line, + bool is_first_formatted_line, + bool use_first_line_style, bool is_after_forced_break); // Use ::first-line style if true. @@ -158,6 +167,7 @@ class CORE_EXPORT NGLineInfo { void SetLineEndFragment(scoped_refptr<NGPhysicalTextFragment>); private: + const NGInlineItemsData* items_data_ = nullptr; const ComputedStyle* line_style_ = nullptr; NGInlineItemResults results_; scoped_refptr<NGPhysicalTextFragment> line_end_fragment_; 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 be19e467b59..7fad403e72e 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 @@ -20,27 +20,46 @@ NGInlineItemsBuilderTemplate< template <typename OffsetMappingBuilder> String NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::ToString() { - // Segment Break Transformation Rules[1] defines to keep trailing new lines, - // but it will be removed in Phase II[2]. We prefer not to add trailing new - // lines and collapsible spaces in Phase I. + // Segment Break Transformation Rules[1] defines to keep trailing new lines in + // Phase I, but to remove after line break, in Phase II[2]. Although the spec + // defines so, trailing collapsible spaces at the end of an inline formatting + // context will be removed in Phase II and that removing here makes no + // differences. + // + // However, doing so reduces the opportunities to re-use NGInlineItem a lot in + // appending scenario, which is quite common. In order to re-use NGInlineItem + // as much as posssible, trailing spaces are removed in Phase II, exactly as + // defined in the spec. + // // [1] https://drafts.csswg.org/css-text-3/#line-break-transform // [2] https://drafts.csswg.org/css-text-3/#white-space-phase-2 + return text_.ToString(); +} + +template <> +String NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::ToString() { + // While trailing collapsible space is kept as above, NGOffsetMappingBuilder + // assumes NGLineBreaker does not remove it. For now, remove only for + // NGOffsetMappingBuilder. + // TODO(kojii): Consider NGOffsetMappingBuilder to support NGLineBreaker to + // remove trailing spaces. RemoveTrailingCollapsibleSpaceIfExists(); return text_.ToString(); } +namespace { // Determine "Ambiguous" East Asian Width is Wide or Narrow. // Unicode East Asian Width // http://unicode.org/reports/tr11/ -static bool IsAmbiguosEastAsianWidthWide(const ComputedStyle* style) { +bool IsAmbiguosEastAsianWidthWide(const ComputedStyle* style) { UScriptCode script = style->GetFontDescription().GetScript(); return script == USCRIPT_KATAKANA_OR_HIRAGANA || script == USCRIPT_SIMPLIFIED_HAN || script == USCRIPT_TRADITIONAL_HAN; } // Determine if a character has "Wide" East Asian Width. -static bool IsEastAsianWidthWide(UChar32 c, const ComputedStyle* style) { +bool IsEastAsianWidthWide(UChar32 c, const ComputedStyle* style) { UEastAsianWidth eaw = static_cast<UEastAsianWidth>( u_getIntPropertyValue(c, UCHAR_EAST_ASIAN_WIDTH)); return eaw == U_EA_WIDE || eaw == U_EA_FULLWIDTH || eaw == U_EA_HALFWIDTH || @@ -51,11 +70,11 @@ static bool IsEastAsianWidthWide(UChar32 c, const ComputedStyle* style) { // Determine whether a newline should be removed or not. // CSS Text, Segment Break Transformation Rules // https://drafts.csswg.org/css-text-3/#line-break-transform -static bool ShouldRemoveNewlineSlow(const StringBuilder& before, - unsigned space_index, - const ComputedStyle* before_style, - const StringView& after, - const ComputedStyle* after_style) { +bool ShouldRemoveNewlineSlow(const StringBuilder& before, + unsigned space_index, + const ComputedStyle* before_style, + const StringView& after, + const ComputedStyle* after_style) { // Remove if either before/after the newline is zeroWidthSpaceCharacter. UChar32 last = 0; DCHECK(space_index == before.length() || @@ -95,27 +114,29 @@ static bool ShouldRemoveNewlineSlow(const StringBuilder& before, return false; } -static bool ShouldRemoveNewline(const StringBuilder& before, - unsigned space_index, - const ComputedStyle* before_style, - const StringView& after, - const ComputedStyle* after_style) { +bool ShouldRemoveNewline(const StringBuilder& before, + unsigned space_index, + const ComputedStyle* before_style, + const StringView& after, + const ComputedStyle* after_style) { // All characters before/after removable newline are 16 bits. return (!before.Is8Bit() || !after.Is8Bit()) && ShouldRemoveNewlineSlow(before, space_index, before_style, after, after_style); } -static void AppendItem(Vector<NGInlineItem>* items, - NGInlineItem::NGInlineItemType type, - unsigned start, - unsigned end, - const ComputedStyle* style = nullptr, - LayoutObject* layout_object = nullptr) { - items->push_back(NGInlineItem(type, start, end, style, layout_object)); +void AppendItem(Vector<NGInlineItem>* items, + NGInlineItem::NGInlineItemType type, + unsigned start, + unsigned end, + const ComputedStyle* style = nullptr, + LayoutObject* layout_object = nullptr, + bool end_may_collapse = false) { + items->push_back( + NGInlineItem(type, start, end, style, layout_object, end_may_collapse)); } -static inline bool ShouldIgnore(UChar c) { +inline bool ShouldIgnore(UChar c) { // Ignore carriage return and form feed. // https://drafts.csswg.org/css-text-3/#white-space-processing // https://github.com/w3c/csswg-drafts/issues/855 @@ -126,14 +147,14 @@ static inline bool ShouldIgnore(UChar c) { return c == kCarriageReturnCharacter || c == kFormFeedCharacter; } -static inline bool IsCollapsibleSpace(UChar c) { +inline bool IsCollapsibleSpace(UChar c) { return c == kSpaceCharacter || c == kNewlineCharacter || c == kTabulationCharacter || c == kCarriageReturnCharacter; } // Characters needing a separate control item than other text items. // It makes the line breaker easier to handle. -static inline bool IsControlItemCharacter(UChar c) { +inline bool IsControlItemCharacter(UChar c) { return c == kNewlineCharacter || c == kTabulationCharacter || // Include ignorable character here to avoids shaping/rendering // these glyphs, and to help the line breaker to ignore them. @@ -143,9 +164,9 @@ static inline bool IsControlItemCharacter(UChar c) { // Find the end of the collapsible spaces. // Returns whether this space run contains a newline or not, because it changes // the collapsing behavior. -static inline bool MoveToEndOfCollapsibleSpaces(const StringView& string, - unsigned* offset, - UChar* c) { +inline bool MoveToEndOfCollapsibleSpaces(const StringView& string, + unsigned* offset, + UChar* c) { DCHECK_EQ(*c, string[*offset]); DCHECK(IsCollapsibleSpace(*c)); bool space_run_has_newline = *c == kNewlineCharacter; @@ -161,7 +182,7 @@ static inline bool MoveToEndOfCollapsibleSpaces(const StringView& string, // Find the last item to compute collapsing with. Opaque items such as // open/close or bidi controls are ignored. // Returns nullptr if there were no previous items. -static NGInlineItem* LastItemToCollapseWith(Vector<NGInlineItem>* items) { +NGInlineItem* LastItemToCollapseWith(Vector<NGInlineItem>* items) { for (auto it = items->rbegin(); it != items->rend(); it++) { NGInlineItem& item = *it; if (item.EndCollapseType() != NGInlineItem::kOpaqueToCollapsing) @@ -170,6 +191,66 @@ static NGInlineItem* LastItemToCollapseWith(Vector<NGInlineItem>* items) { return nullptr; } +inline bool MayCollapseWithLast(const NGInlineItem* item) { + return item && item->EndMayCollapse(); +} + +} // anonymous namespace + +template <typename OffsetMappingBuilder> +bool NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::Append( + const String& original_string, + LayoutObject* layout_object, + const Vector<NGInlineItem*>& items) { + // Don't reuse existing items if they might be affected by whitespace + // collapsing. + // TODO(layout-dev): This could likely be optimized further. + // TODO(layout-dev): Handle cases where the old items are not consecutive. + if (MayCollapseWithLast(LastItemToCollapseWith(items_)) || + IsCollapsibleSpace(original_string[items[0]->StartOffset()])) + return false; + + for (const NGInlineItem* item : items) { + unsigned start = text_.length(); + text_.Append(original_string, item->StartOffset(), item->Length()); + + // If the item's position within the container remains unchanged the item + // itself may be reused. + if (item->StartOffset() == start) { + items_->push_back(*item); + is_empty_inline_ &= item->IsEmptyItem(); + continue; + } + + // If the position has shifted the item and the shape result needs to be + // adjusted to reflect the new start and end offsets. + unsigned end = start + item->Length(); + DCHECK(item->TextShapeResult()); + NGInlineItem adjusted_item( + *item, start, end, item->TextShapeResult()->CopyAdjustedOffset(start)); + + DCHECK(adjusted_item.TextShapeResult()); + DCHECK_EQ(start, adjusted_item.StartOffset()); + DCHECK_EQ(start, adjusted_item.TextShapeResult()->StartIndexForResult()); + DCHECK_EQ(end, adjusted_item.EndOffset()); + DCHECK_EQ(end, adjusted_item.TextShapeResult()->EndIndexForResult()); + DCHECK_EQ(item->IsEmptyItem(), adjusted_item.IsEmptyItem()); + + items_->push_back(adjusted_item); + is_empty_inline_ &= adjusted_item.IsEmptyItem(); + } + return true; +} + +template <> +bool NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::Append( + const String&, + LayoutObject*, + const Vector<NGInlineItem*>&) { + NOTREACHED(); + return false; +} + template <typename OffsetMappingBuilder> void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::Append( const String& string, @@ -371,7 +452,7 @@ void NGInlineItemsBuilderTemplate< if (text_.length() > start_offset) { AppendItem(items_, NGInlineItem::kText, start_offset, text_.length(), style, - layout_object); + layout_object, end_collapse != NGInlineItem::kNotCollapsible); NGInlineItem& item = items_->back(); item.SetEndCollapseType(end_collapse); is_empty_inline_ &= item.IsEmptyItem(); 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 f041c6b9f63..53e34b6cef3 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 @@ -19,7 +19,6 @@ namespace blink { class ComputedStyle; class LayoutObject; class LayoutText; -class NGInlineItem; // NGInlineItemsBuilder builds a string and a list of NGInlineItem from inlines. // @@ -54,6 +53,12 @@ class CORE_TEMPLATE_CLASS_EXPORT NGInlineItemsBuilderTemplate { // <span></span> or <span><float></float></span>. bool IsEmptyInline() const { return is_empty_inline_; } + // 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 Append(const String&, LayoutObject*, const Vector<NGInlineItem*>&); + // Append a string. // When appending, spaces are collapsed according to CSS Text, The white space // processing rules @@ -149,6 +154,15 @@ class CORE_TEMPLATE_CLASS_EXPORT NGInlineItemsBuilderTemplate { void Exit(LayoutObject*); }; +template <> +String NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::ToString(); + +template <> +bool NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::Append( + const String&, + LayoutObject*, + const Vector<NGInlineItem*>&); + extern template class CORE_EXTERN_TEMPLATE_EXPORT NGInlineItemsBuilderTemplate<EmptyOffsetMappingBuilder>; extern template class CORE_EXTERN_TEMPLATE_EXPORT 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 d3b4b9eff09..ae5a273962c 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 @@ -8,6 +8,7 @@ #include "third_party/blink/renderer/core/layout/layout_inline.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/core/testing/page_test_base.h" namespace blink { @@ -18,13 +19,6 @@ namespace { EXPECT_EQ(start, (item).StartOffset()); \ EXPECT_EQ(end, (item).EndOffset()); -static scoped_refptr<ComputedStyle> CreateWhitespaceStyle( - EWhiteSpace whitespace) { - scoped_refptr<ComputedStyle> style(ComputedStyle::Create()); - style->SetWhiteSpace(whitespace); - return style; -} - static String GetCollapsed(const NGOffsetMappingBuilder& builder) { Vector<unsigned> mapping = builder.DumpOffsetMappingForTesting(); @@ -47,40 +41,60 @@ static String GetCollapsed(const NGOffsetMappingBuilder& builder) { return result.ToString(); } -class NGInlineItemsBuilderTest : public testing::Test { +class NGInlineItemsBuilderTest : public PageTestBase { protected: - void SetUp() override { style_ = ComputedStyle::Create(); } + void SetUp() override { + PageTestBase::SetUp(); + style_ = ComputedStyle::Create(); + } void SetWhiteSpace(EWhiteSpace whitespace) { style_->SetWhiteSpace(whitespace); } - const String& TestAppend(const String inputs[], int size) { + scoped_refptr<ComputedStyle> GetStyle(EWhiteSpace whitespace) { + if (whitespace == EWhiteSpace::kNormal) + return style_; + scoped_refptr<ComputedStyle> style(ComputedStyle::Create()); + style->SetWhiteSpace(whitespace); + return style; + } + + struct Input { + const String text; + EWhiteSpace whitespace = EWhiteSpace::kNormal; + LayoutText* layout_text = nullptr; + }; + + const String& TestAppend(Vector<Input> inputs) { items_.clear(); NGInlineItemsBuilderForOffsetMapping builder(&items_); - for (int i = 0; i < size; i++) - builder.Append(inputs[i], style_.get()); + for (Input& input : inputs) { + if (!input.layout_text) + input.layout_text = LayoutText::CreateEmptyAnonymous(GetDocument()); + builder.Append(input.text, GetStyle(input.whitespace).get(), + input.layout_text); + } text_ = builder.ToString(); collapsed_ = GetCollapsed(builder.GetOffsetMappingBuilder()); ValidateItems(); + CheckReuseItemsProducesSameResult(inputs); return text_; } const String& TestAppend(const String& input) { - String inputs[] = {input}; - return TestAppend(inputs, 1); + return TestAppend({Input{input}}); + } + const String& TestAppend(const Input& input1, const Input& input2) { + return TestAppend({input1, input2}); } - const String& TestAppend(const String& input1, const String& input2) { - String inputs[] = {input1, input2}; - return TestAppend(inputs, 2); + return TestAppend(Input{input1}, Input{input2}); } - const String& TestAppend(const String& input1, const String& input2, const String& input3) { - String inputs[] = {input1, input2, input3}; - return TestAppend(inputs, 3); + return TestAppend({{input1}, {input2}, {input3}}); } void ValidateItems() { @@ -94,6 +108,35 @@ class NGInlineItemsBuilderTest : public testing::Test { EXPECT_EQ(current_offset, text_.length()); } + void CheckReuseItemsProducesSameResult(Vector<Input> inputs) { + Vector<NGInlineItem> reuse_items; + NGInlineItemsBuilder reuse_builder(&reuse_items); + for (Input& input : inputs) { + // Collect items for this LayoutObject. + DCHECK(input.layout_text); + Vector<NGInlineItem*> previous_items; + for (auto& item : items_) { + if (item.GetLayoutObject() == input.layout_text) + previous_items.push_back(&item); + } + + // Try to re-use previous items, or Append if it was not re-usable. + bool reused = !previous_items.IsEmpty() && + reuse_builder.Append(text_, nullptr, previous_items); + if (!reused) + reuse_builder.Append(input.text, style_.get()); + } + + // Currently, NGInlineItemsBuilder does not strip trailing spaces while + // NGInlineItemsBuilderForOffsetMapping does. See + // NGInlineItemsBuilderTemplate<NGOffsetMappingBuilder>::ToString(). + String reuse_text = reuse_builder.ToString(); + if (!reuse_text.IsEmpty() && reuse_text != text_ && + reuse_text[reuse_text.length() - 1] == kSpaceCharacter) + reuse_text = reuse_text.Substring(0, reuse_text.length() - 1); + EXPECT_EQ(text_, reuse_text); + } + Vector<NGInlineItem> items_; String text_; String collapsed_; @@ -254,15 +297,11 @@ TEST_F(NGInlineItemsBuilderTest, CollapseBeforeAndAfterNewline) { TEST_F(NGInlineItemsBuilderTest, CollapsibleSpaceAfterNonCollapsibleSpaceAcrossElements) { - NGInlineItemsBuilderForOffsetMapping builder(&items_); - scoped_refptr<ComputedStyle> pre_wrap( - CreateWhitespaceStyle(EWhiteSpace::kPreWrap)); - builder.Append("text ", pre_wrap.get()); - builder.Append(" text", style_.get()); - EXPECT_EQ("text text", builder.ToString()) + EXPECT_EQ("text text", + TestAppend({"text ", EWhiteSpace::kPreWrap}, {" text"})) << "The whitespace in constructions like '<span style=\"white-space: " "pre-wrap\">text <span><span> text</span>' does not collapse."; - EXPECT_EQ("{}", GetCollapsed(builder.GetOffsetMappingBuilder())); + EXPECT_EQ("{}", collapsed_); } TEST_F(NGInlineItemsBuilderTest, CollapseZeroWidthSpaces) { @@ -419,16 +458,11 @@ INSTANTIATE_TEST_CASE_P(NGInlineItemsBuilderTest, TEST_P(CollapsibleSpaceTest, CollapsedSpaceAfterNoWrap) { UChar space = GetParam(); - Vector<NGInlineItem> items; - NGInlineItemsBuilderForOffsetMapping builder(&items); - scoped_refptr<ComputedStyle> nowrap_style(ComputedStyle::Create()); - nowrap_style->SetWhiteSpace(EWhiteSpace::kNowrap); - builder.Append(String("nowrap") + space, nowrap_style.get()); - builder.Append(" wrap", style_.get()); - EXPECT_EQ(String("nowrap " - u"\u200B" - "wrap"), - builder.ToString()); + EXPECT_EQ( + String("nowrap " + u"\u200B" + "wrap"), + TestAppend({String("nowrap") + space, EWhiteSpace::kNowrap}, {" wrap"})); } TEST_F(NGInlineItemsBuilderTest, BidiBlockOverride) { 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 5b4a1d99f34..0d04530a405 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 @@ -4,30 +4,27 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.h" -#include <algorithm> -#include <limits> #include <memory> -#include <utility> #include "third_party/blink/renderer/core/layout/ng/inline/ng_baseline.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_box_state.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_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_box_fragment.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_breaker.h" -#include "third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_line_truncator.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_fragment_builder.h" #include "third_party/blink/renderer/core/layout/ng/list/layout_ng_list_marker.h" #include "third_party/blink/renderer/core/layout/ng/list/ng_unpositioned_list_marker.h" -#include "third_party/blink/renderer/core/layout/ng/ng_block_layout_algorithm.h" #include "third_party/blink/renderer/core/layout/ng/ng_box_fragment.h" #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space.h" #include "third_party/blink/renderer/core/layout/ng/ng_floats_utils.h" -#include "third_party/blink/renderer/core/layout/ng/ng_fragment_builder.h" #include "third_party/blink/renderer/core/layout/ng/ng_layout_result.h" #include "third_party/blink/renderer/core/layout/ng/ng_length_utils.h" +#include "third_party/blink/renderer/core/layout/ng/ng_positioned_float.h" #include "third_party/blink/renderer/core/layout/ng/ng_space_utils.h" #include "third_party/blink/renderer/core/layout/ng/ng_unpositioned_float.h" #include "third_party/blink/renderer/core/style/computed_style.h" @@ -42,8 +39,12 @@ struct NGLineAlign { NGLineAlign(const NGLineInfo&); NGLineAlign() = delete; + // The space to align or justify. This includes trailing spaces if exists. LayoutUnit space; + + // The end offset with trailing spaces excluded. unsigned end_offset; + LayoutUnit trailing_spaces_width; }; NGLineAlign::NGLineAlign(const NGLineInfo& line_info) { @@ -55,14 +56,16 @@ NGLineAlign::NGLineAlign(const NGLineInfo& line_info) { const NGInlineItemResult& item_result = *it; if (!item_result.has_only_trailing_spaces) { end_offset = item_result.end_offset; + space += trailing_spaces_width; return; } - space += item_result.inline_size; + trailing_spaces_width += item_result.inline_size; } // An empty line, or only trailing spaces. DCHECK_EQ(space, line_info.AvailableWidth() - line_info.TextIndent()); end_offset = line_info.StartOffset(); + space += trailing_spaces_width; } } // namespace @@ -80,15 +83,16 @@ NGInlineLayoutAlgorithm::NGInlineLayoutAlgorithm( // lays out in visual order. TextDirection::kLtr, break_token), + baseline_type_(container_builder_.Style().GetFontBaseline()), is_horizontal_writing_mode_( blink::IsHorizontalWritingMode(space.GetWritingMode())) { quirks_mode_ = inline_node.InLineHeightQuirksMode(); - unpositioned_floats_ = ConstraintSpace().UnpositionedFloats(); - - if (!is_horizontal_writing_mode_) - baseline_type_ = FontBaseline::kIdeographicBaseline; } +// Define the destructor here, so that we can forward-declare more in the +// header. +NGInlineLayoutAlgorithm::~NGInlineLayoutAlgorithm() = default; + NGInlineBoxState* NGInlineLayoutAlgorithm::HandleOpenTag( const NGInlineItem& item, const NGInlineItemResult& item_result) { @@ -127,7 +131,7 @@ void NGInlineLayoutAlgorithm::PrepareBoxStates( DCHECK(break_token->UseFirstLineStyle()); // Compute which tags are not closed at the beginning of this line. - const Vector<NGInlineItem>& items = Node().Items(); + const Vector<NGInlineItem>& items = line_info.ItemsData().items; Vector<const NGInlineItem*, 16> open_items; for (unsigned i = 0; i < break_token->ItemIndex(); i++) { const NGInlineItem& item = items[i]; @@ -191,12 +195,13 @@ void NGInlineLayoutAlgorithm::CreateLine(NGLineInfo* line_info, baseline_type_); } - text_builder.SetItem(NGPhysicalTextFragment::kNormalText, &item_result, + text_builder.SetItem(NGPhysicalTextFragment::kNormalText, + line_info->ItemsData(), &item_result, box->text_height); line_box_.AddChild(text_builder.ToTextFragment(), box->text_top, item_result.inline_size, item.BidiLevel()); } else if (item.Type() == NGInlineItem::kControl) { - PlaceControlItem(item, &item_result, box); + PlaceControlItem(item, *line_info, &item_result, box); } else if (item.Type() == NGInlineItem::kOpenTag) { box = HandleOpenTag(item, item_result); } else if (item.Type() == NGInlineItem::kCloseTag) { @@ -244,6 +249,19 @@ void NGInlineLayoutAlgorithm::CreateLine(NGLineInfo* line_info, BidiReorder(); box_states_->UpdateAfterReorder(&line_box_); LayoutUnit inline_size = box_states_->ComputeInlinePositions(&line_box_); + + // Truncate the line if 'text-overflow: ellipsis' is set. + if (UNLIKELY(inline_size > line_info->AvailableWidth() && + node_.GetLayoutBlockFlow()->ShouldTruncateOverflowingText())) { + inline_size = NGLineTruncator(node_, *line_info) + .TruncateLine(inline_size, &line_box_); + } + + // Create box fragmetns if needed. After this point forward, |line_box_| is a + // tree structure. + if (box_states_->HasBoxFragments()) + box_states_->CreateBoxFragments(&line_box_); + const NGLineHeightMetrics& line_box_metrics = box_states_->LineBoxState().metrics; @@ -280,6 +298,8 @@ void NGInlineLayoutAlgorithm::CreateLine(NGLineInfo* line_info, if (IsLtr(line_info->BaseDirection())) line_bfc_offset.line_offset += line_info->TextIndent(); + if (line_info->UseFirstLineStyle()) + container_builder_.SetStyleVariant(NGStyleVariant::kFirstLine); container_builder_.AddChildren(line_box_); container_builder_.SetInlineSize(inline_size); container_builder_.SetBaseDirection(line_info->BaseDirection()); @@ -288,12 +308,13 @@ void NGInlineLayoutAlgorithm::CreateLine(NGLineInfo* line_info, } void NGInlineLayoutAlgorithm::PlaceControlItem(const NGInlineItem& item, + const NGLineInfo& line_info, NGInlineItemResult* item_result, NGInlineBoxState* box) { DCHECK_EQ(item.Type(), NGInlineItem::kControl); DCHECK_EQ(item.Length(), 1u); DCHECK(!item.TextShapeResult()); - UChar character = Node().Text()[item.StartOffset()]; + UChar character = line_info.ItemsData().text_content[item.StartOffset()]; NGPhysicalTextFragment::NGTextType type; switch (character) { case kNewlineCharacter: @@ -321,7 +342,8 @@ void NGInlineLayoutAlgorithm::PlaceControlItem(const NGInlineItem& item, NGTextFragmentBuilder text_builder(Node(), ConstraintSpace().GetWritingMode()); - text_builder.SetItem(type, item_result, box->text_height); + text_builder.SetItem(type, line_info.ItemsData(), item_result, + box->text_height); line_box_.AddChild(text_builder.ToTextFragment(), box->text_top, item_result->inline_size, item.BidiLevel()); } @@ -450,7 +472,9 @@ bool NGInlineLayoutAlgorithm::ApplyJustify(NGLineInfo* line_info) { // Construct the line text to compute spacing for. String line_text = - Node().Text(line_info->StartOffset(), align.end_offset).ToString(); + StringView(line_info->ItemsData().text_content, line_info->StartOffset(), + align.end_offset - line_info->StartOffset()) + .ToString(); // Append a hyphen if the last word is hyphenated. The hyphen is in // |ShapeResult|, but not in text. |ShapeResultSpacing| needs the text that @@ -498,8 +522,9 @@ LayoutUnit NGInlineLayoutAlgorithm::OffsetForTextAlign( // Justification is applied in earlier phase, see PlaceItems(). DCHECK_NE(text_align, ETextAlign::kJustify); + NGLineAlign align(line_info); return LineOffsetForTextAlign(text_align, line_info.BaseDirection(), - NGLineAlign(line_info).space); + align.space, align.trailing_spaces_width); } LayoutUnit NGInlineLayoutAlgorithm::ComputeContentSize( @@ -538,10 +563,20 @@ scoped_refptr<NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { bool is_empty_inline = Node().IsEmptyInline(); - if (!is_empty_inline) { - DCHECK(ConstraintSpace().UnpositionedFloats().IsEmpty()); + if (is_empty_inline) { + // We're just going to collapse through this one, so whatever went in on one + // side will go out on the other side. The position of the adjoining floats + // will be affected by any subsequent block, until the BFC offset is + // resolved. + container_builder_.AddAdjoiningFloatTypes( + ConstraintSpace().AdjoiningFloatTypes()); + } else { DCHECK(ConstraintSpace().MarginStrut().IsEmpty()); container_builder_.SetBfcOffset(ConstraintSpace().BfcOffset()); + + // The BFC offset was determined before entering this algorithm. This means + // that there should be no adjoining floats. + DCHECK(!ConstraintSpace().AdjoiningFloatTypes()); } // In order to get the correct list of layout opportunities, we need to @@ -552,10 +587,9 @@ scoped_refptr<NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { // If we are an empty inline, we don't have to run the full algorithm, we can // return now as we should have positioned all of our floats. if (is_empty_inline) { - DCHECK_EQ(handled_item_index, Node().Items().size()); + DCHECK_EQ(handled_item_index, Node().ItemsData(false).items.size()); container_builder_.SwapPositionedFloats(&positioned_floats_); - container_builder_.SwapUnpositionedFloats(&unpositioned_floats_); container_builder_.SetEndMarginStrut(ConstraintSpace().MarginStrut()); container_builder_.SetExclusionSpace(std::move(initial_exclusion_space)); @@ -572,7 +606,7 @@ scoped_refptr<NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { // 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. - Vector<NGLayoutOpportunity> opportunities = + const Vector<NGLayoutOpportunity> opportunities = initial_exclusion_space->AllLayoutOpportunities( ConstraintSpace().BfcOffset(), ConstraintSpace().AvailableSize().inline_size); @@ -583,7 +617,30 @@ scoped_refptr<NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { std::unique_ptr<NGExclusionSpace> exclusion_space; NGInlineBreakToken* break_token = BreakToken(); - for (const auto& opportunity : opportunities) { + LayoutUnit line_block_size; + LayoutUnit block_delta; + const auto* opportunities_it = opportunities.begin(); + while (opportunities_it != opportunities.end()) { + const NGLayoutOpportunity& opportunity = *opportunities_it; + +#if DCHECK_IS_ON() + // Make sure the last opportunity has the correct properties. + if (opportunities_it + 1 == opportunities.end()) { + // We shouldn't have any shapes affecting the last opportunity. + DCHECK(!opportunity.HasShapeExclusions()); + DCHECK_EQ(line_block_size, LayoutUnit()); + DCHECK_EQ(block_delta, LayoutUnit()); + + // The opportunity should match the given available size, (however need + // to check if the inline-size got saturated first). + if (opportunity.rect.InlineSize() != LayoutUnit::Max()) { + DCHECK_EQ(opportunity.rect.InlineSize(), + ConstraintSpace().AvailableSize().inline_size); + } + DCHECK_EQ(opportunity.rect.BlockSize(), LayoutUnit::Max()); + } +#endif + // Reset any state that may have been modified in a previous pass. positioned_floats.clear(); unpositioned_floats_.clear(); @@ -591,24 +648,44 @@ scoped_refptr<NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { exclusion_space = std::make_unique<NGExclusionSpace>(*initial_exclusion_space); + NGLineLayoutOpportunity line_opportunity = + opportunity.ComputeLineLayoutOpportunity(ConstraintSpace(), + line_block_size, block_delta); + NGLineInfo line_info; - NGLineBreaker line_breaker(Node(), NGLineBreakerMode::kContent, - constraint_space_, &positioned_floats, - &unpositioned_floats_, exclusion_space.get(), - handled_item_index, break_token); + NGLineBreaker line_breaker( + Node(), NGLineBreakerMode::kContent, constraint_space_, + &positioned_floats, &unpositioned_floats_, &container_builder_, + exclusion_space.get(), handled_item_index, break_token); // TODO(ikilpatrick): Does this always succeed when we aren't an empty // inline? - if (!line_breaker.NextLine(opportunity, &line_info)) + if (!line_breaker.NextLine(line_opportunity, &line_info)) break; // If this fragment will be larger than the inline-size of the opportunity, - // *and* the opportunity is smaller than the available inline-size, - // continue to the next opportunity. - if (line_info.Width() > opportunity.rect.InlineSize() && - opportunity.rect.InlineSize() != - ConstraintSpace().AvailableSize().inline_size) + // *and* the opportunity is smaller than the available inline-size, and the + // container autowraps, continue to the next opportunity. + if (line_info.Width() > line_opportunity.AvailableInlineSize() && + ConstraintSpace().AvailableSize().inline_size != + line_opportunity.AvailableFloatInlineSize() && + 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. + if (UNLIKELY(opportunity.HasShapeExclusions()) && + block_delta < opportunity.rect.BlockSize() && + !opportunity.IsBlockDeltaBelowShapes(block_delta)) { + block_delta += LayoutUnit(1); + line_block_size = LayoutUnit(); + } else { + // We've either don't have any shapes, or run out of block-delta space + // to test, proceed to the next layout opportunity. + block_delta = LayoutUnit(); + line_block_size = LayoutUnit(); + ++opportunities_it; + } continue; + } PrepareBoxStates(line_info, break_token); CreateLine(&line_info, exclusion_space.get()); @@ -616,8 +693,33 @@ scoped_refptr<NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { // We now can check the block-size of the fragment, and it fits within the // opportunity. LayoutUnit block_size = container_builder_.ComputeBlockSize(); - if (block_size > opportunity.rect.BlockSize()) + + // Now that we have the block-size of the line, we can re-test the layout + // opportunity to see if we fit into the (potentially) non-rectangular + // shape area. + // If the AvailableInlineSize changes we need to run the line breaker again + // with the calculated line_block_size. This is *safe* as the line breaker + // won't produce a line which has a larger block-size, (as it can only + // decrease or stay the same size). + if (UNLIKELY(opportunity.HasShapeExclusions())) { + NGLineLayoutOpportunity line_opportunity_with_height = + opportunity.ComputeLineLayoutOpportunity(ConstraintSpace(), + block_size, block_delta); + + if (line_opportunity_with_height.AvailableInlineSize() != + line_opportunity.AvailableInlineSize()) { + line_block_size = block_size; + continue; + } + } + + // Check if the line will fit in the current opportunity. + if (block_size + block_delta > opportunity.rect.BlockSize()) { + block_delta = LayoutUnit(); + line_block_size = LayoutUnit(); + ++opportunities_it; continue; + } if (opportunity.rect.BlockStartOffset() > ConstraintSpace().BfcOffset().block_offset) @@ -662,7 +764,7 @@ scoped_refptr<NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { // TODO(ikilpatrick): Do we need to always add the OOFs here? unsigned NGInlineLayoutAlgorithm::PositionLeadingItems( NGExclusionSpace* exclusion_space) { - const Vector<NGInlineItem>& items = Node().Items(); + const Vector<NGInlineItem>& items = Node().ItemsData(false).items; bool is_empty_inline = Node().IsEmptyInline(); LayoutUnit bfc_line_offset = ConstraintSpace().BfcOffset().line_offset; @@ -675,10 +777,12 @@ unsigned NGInlineLayoutAlgorithm::PositionLeadingItems( NGBoxStrut margins = ComputeMarginsForContainer(ConstraintSpace(), node.Style()); - unpositioned_floats_.push_back(NGUnpositionedFloat::Create( + auto unpositioned_float = NGUnpositionedFloat::Create( ConstraintSpace().AvailableSize(), ConstraintSpace().PercentageResolutionSize(), bfc_line_offset, - bfc_line_offset, margins, node, /* break_token */ nullptr)); + bfc_line_offset, margins, node, /* break_token */ nullptr); + AddUnpositionedFloat(&unpositioned_floats_, &container_builder_, + std::move(unpositioned_float)); } else if (is_empty_inline && item.Type() == NGInlineItem::kOutOfFlowPositioned) { NGBlockNode node(ToLayoutBox(item.GetLayoutObject())); 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 b0c9a2edb91..017544f453a 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 @@ -6,9 +6,6 @@ #define NGInlineLayoutAlgorithm_h #include "third_party/blink/renderer/core/core_export.h" -#include "third_party/blink/renderer/core/layout/ng/geometry/ng_logical_offset.h" -#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.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_box_fragment_builder.h" #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h" @@ -23,7 +20,10 @@ class NGConstraintSpace; class NGInlineBreakToken; class NGInlineNode; class NGInlineItem; -class NGLineBoxFragmentBuilder; +class NGInlineLayoutStateStack; +class NGLineInfo; +struct NGInlineBoxState; +struct NGInlineItemResult; struct NGPositionedFloat; // A class for laying out an inline formatting context, i.e. a block with inline @@ -40,6 +40,7 @@ class CORE_EXPORT NGInlineLayoutAlgorithm final NGInlineLayoutAlgorithm(NGInlineNode, const NGConstraintSpace&, NGInlineBreakToken* = nullptr); + ~NGInlineLayoutAlgorithm() override; void CreateLine(NGLineInfo*, NGExclusionSpace*); @@ -59,6 +60,7 @@ class CORE_EXPORT NGInlineLayoutAlgorithm final void BidiReorder(); void PlaceControlItem(const NGInlineItem&, + const NGLineInfo&, NGInlineItemResult*, NGInlineBoxState*); void PlaceGeneratedContent(scoped_refptr<NGPhysicalFragment>, 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 ed5f88b97c1..654b7eb887e 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 @@ -5,14 +5,12 @@ #include "third_party/blink/renderer/core/layout/ng/ng_base_layout_algorithm_test.h" #include "third_party/blink/renderer/core/dom/tag_collection.h" -#include "third_party/blink/renderer/core/layout/line/inline_text_box.h" +#include "third_party/blink/renderer/core/layout/layout_block_flow.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.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/inline/ng_physical_text_fragment.h" -#include "third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.h" -#include "third_party/blink/renderer/core/layout/ng/ng_block_break_token.h" #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h" #include "third_party/blink/renderer/core/layout/ng/ng_layout_result.h" #include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h" 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 92aabbded97..bf394eba54f 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 @@ -5,39 +5,28 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h" #include <algorithm> -#include <limits> #include <memory> -#include <utility> +#include "third_party/blink/renderer/core/layout/layout_block_flow.h" #include "third_party/blink/renderer/core/layout/layout_object.h" #include "third_party/blink/renderer/core/layout/layout_text.h" -#include "third_party/blink/renderer/core/layout/layout_text_fragment.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_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" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm.h" -#include "third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h" -#include "third_party/blink/renderer/core/layout/ng/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/inline/ng_text_fragment.h" -#include "third_party/blink/renderer/core/layout/ng/layout_ng_block_flow.h" #include "third_party/blink/renderer/core/layout/ng/legacy_layout_tree_walking.h" -#include "third_party/blink/renderer/core/layout/ng/list/layout_ng_list_item.h" -#include "third_party/blink/renderer/core/layout/ng/ng_box_fragment.h" #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h" -#include "third_party/blink/renderer/core/layout/ng/ng_fragment_builder.h" -#include "third_party/blink/renderer/core/layout/ng/ng_fragmentation_utils.h" #include "third_party/blink/renderer/core/layout/ng/ng_layout_result.h" #include "third_party/blink/renderer/core/layout/ng/ng_length_utils.h" -#include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h" #include "third_party/blink/renderer/core/layout/ng/ng_positioned_float.h" +#include "third_party/blink/renderer/core/layout/ng/ng_unpositioned_float.h" #include "third_party/blink/renderer/core/style/computed_style.h" #include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h" #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h" -#include "third_party/blink/renderer/platform/runtime_enabled_features.h" #include "third_party/blink/renderer/platform/wtf/text/character_names.h" namespace blink { @@ -49,6 +38,11 @@ template <typename OffsetMappingBuilder> void ClearNeedsLayoutIfUpdatingLayout(LayoutObject* node) { node->ClearNeedsLayout(); node->ClearNeedsCollectInlines(); + // Reset previous items if they cannot be reused to prevent stale items + // for subsequent layouts. Items that can be reused have already been + // added to the builder. + if (node->IsLayoutNGText()) + ToLayoutNGText(node)->ClearInlineItems(); } template <> @@ -67,19 +61,33 @@ void ClearNeedsLayoutIfUpdatingLayout<NGOffsetMappingBuilder>(LayoutObject*) {} template <typename OffsetMappingBuilder> void CollectInlinesInternal( LayoutBlockFlow* block, - NGInlineItemsBuilderTemplate<OffsetMappingBuilder>* builder) { + NGInlineItemsBuilderTemplate<OffsetMappingBuilder>* builder, + String* previous_text) { builder->EnterBlock(block->Style()); LayoutObject* node = GetLayoutObjectForFirstChildNode(block); while (node) { if (node->IsText()) { LayoutText* layout_text = ToLayoutText(node); - if (UNLIKELY(layout_text->IsWordBreak())) { - builder->AppendBreakOpportunity(node->Style(), layout_text); - } else { - builder->Append(layout_text->GetText(), node->Style(), layout_text); + + // 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 (node->IsLayoutNGText() && ToLayoutNGText(node)->HasValidLayout() && + previous_text) { + item_reused = builder->Append(*previous_text, ToLayoutNGText(node), + ToLayoutNGText(node)->InlineItems()); } - ClearNeedsLayoutIfUpdatingLayout<OffsetMappingBuilder>(layout_text); + // If not create a new item as needed. + if (!item_reused) { + if (UNLIKELY(layout_text->IsWordBreak())) + builder->AppendBreakOpportunity(node->Style(), layout_text); + else + builder->Append(layout_text->GetText(), node->Style(), layout_text); + } + ClearNeedsLayoutIfUpdatingLayout<OffsetMappingBuilder>(layout_text); } else if (node->IsFloating()) { // Add floats and positioned objects in the same way as atomic inlines. @@ -145,6 +153,33 @@ void CollectInlinesInternal( builder->ExitBlock(); } +static bool NeedsShaping(const NGInlineItem& item) { + return item.Type() == NGInlineItem::kText && !item.TextShapeResult(); +} + +// Determine if reshape is needed for ::first-line style. +bool FirstLineNeedsReshape(const ComputedStyle& first_line_style, + const ComputedStyle& base_style) { + const Font& base_font = base_style.GetFont(); + const Font& first_line_font = first_line_style.GetFont(); + return &base_font != &first_line_font && base_font != first_line_font; +} + +// Make a string to the specified length, either by truncating if longer, or +// appending space characters if shorter. +void TruncateOrPadText(String* text, unsigned length) { + if (text->length() > length) { + *text = text->Substring(0, length); + } else if (text->length() < length) { + StringBuilder builder; + builder.ReserveCapacity(length); + builder.Append(*text); + while (builder.length() < length) + builder.Append(kSpaceCharacter); + *text = builder.ToString(); + } +} + } // namespace NGInlineNode::NGInlineNode(LayoutBlockFlow* block) @@ -159,13 +194,29 @@ bool NGInlineNode::InLineHeightQuirksMode() const { return GetDocument().InLineHeightQuirksMode(); } +bool NGInlineNode::CanContainFirstFormattedLine() const { + // TODO(kojii): In LayoutNG, leading OOF creates an anonymous block box, + // and that |LayoutBlockFlow::CanContainFirstFormattedLine()| does not work. + // crbug.com/734554 + LayoutObject* layout_object = GetLayoutBlockFlow(); + if (!layout_object->IsAnonymousBlock()) + return true; + for (;;) { + layout_object = layout_object->PreviousSibling(); + if (!layout_object) + return true; + if (!layout_object->IsFloatingOrOutOfFlowPositioned()) + return false; + } +} + NGInlineNodeData* NGInlineNode::MutableData() { return ToLayoutBlockFlow(box_)->GetNGInlineNodeData(); } bool NGInlineNode::IsPrepareLayoutFinished() const { const NGInlineNodeData* data = ToLayoutBlockFlow(box_)->GetNGInlineNodeData(); - return data && !data->text_content_.IsNull(); + return data && !data->text_content.IsNull(); } const NGInlineNodeData& NGInlineNode::Data() const { @@ -174,28 +225,19 @@ const NGInlineNodeData& NGInlineNode::Data() const { return *ToLayoutBlockFlow(box_)->GetNGInlineNodeData(); } -const Vector<NGInlineItem>& NGInlineNode::Items(bool is_first_line) const { - const NGInlineNodeData& data = Data(); - if (!is_first_line || !data.first_line_items_) - return data.items_; - return *data.first_line_items_; -} - -NGInlineItemRange NGInlineNode::Items(unsigned start, unsigned end) { - return NGInlineItemRange(&MutableData()->items_, start, end); -} - -void NGInlineNode::InvalidatePrepareLayout() { +void NGInlineNode::InvalidatePrepareLayoutForTest() { GetLayoutBlockFlow()->ResetNGInlineNodeData(); DCHECK(!IsPrepareLayoutFinished()); } void NGInlineNode::PrepareLayoutIfNeeded() { + std::unique_ptr<NGInlineNodeData> previous_data; LayoutBlockFlow* block_flow = GetLayoutBlockFlow(); if (IsPrepareLayoutFinished()) { if (!block_flow->NeedsCollectInlines()) return; + previous_data.reset(block_flow->TakeNGInlineNodeData()); block_flow->ResetNGInlineNodeData(); } @@ -203,9 +245,11 @@ void NGInlineNode::PrepareLayoutIfNeeded() { // NGInlineNode represent a collection of adjacent non-atomic inlines. NGInlineNodeData* data = MutableData(); DCHECK(data); - CollectInlines(data); + CollectInlines(data, previous_data.get()); SegmentText(data); - ShapeText(data); + ShapeText(data, previous_data.get()); + ShapeTextForFirstLineIfNeeded(data); + AssociateItemsWithInlines(data); DCHECK_EQ(data, MutableData()); block_flow->ClearNeedsCollectInlines(); @@ -213,10 +257,10 @@ void NGInlineNode::PrepareLayoutIfNeeded() { #if DCHECK_IS_ON() // ComputeOffsetMappingIfNeeded() runs some integrity checks as part of // creating offset mapping. Run the check, and discard the result. - DCHECK(!data->offset_mapping_); + DCHECK(!data->offset_mapping); ComputeOffsetMappingIfNeeded(); - DCHECK(data->offset_mapping_); - data->offset_mapping_.reset(); + DCHECK(data->offset_mapping); + data->offset_mapping.reset(); #endif } @@ -228,45 +272,56 @@ const NGInlineNodeData& NGInlineNode::EnsureData() { const NGOffsetMapping* NGInlineNode::ComputeOffsetMappingIfNeeded() { DCHECK(!GetLayoutBlockFlow()->GetDocument().NeedsLayoutTreeUpdate()); - if (!Data().offset_mapping_) { + NGInlineNodeData* data = MutableData(); + if (!data->offset_mapping) { // TODO(xiaochengh): ComputeOffsetMappingIfNeeded() discards the // NGInlineItems and text content built by |builder|, because they are // already there in NGInlineNodeData. For efficiency, we should make // |builder| not construct items and text content. Vector<NGInlineItem> items; NGInlineItemsBuilderForOffsetMapping builder(&items); - CollectInlinesInternal(GetLayoutBlockFlow(), &builder); - builder.ToString(); + CollectInlinesInternal(GetLayoutBlockFlow(), &builder, nullptr); + String text = builder.ToString(); + + // The trailing space of the text for offset mapping may be removed. If not, + // share the string instance. + if (text == data->text_content) + text = data->text_content; // TODO(xiaochengh): This doesn't compute offset mapping correctly when // text-transform CSS property changes text length. NGOffsetMappingBuilder& mapping_builder = builder.GetOffsetMappingBuilder(); - mapping_builder.SetDestinationString(Text()); - MutableData()->offset_mapping_ = + mapping_builder.SetDestinationString(text); + data->offset_mapping = std::make_unique<NGOffsetMapping>(mapping_builder.Build()); } - return Data().offset_mapping_.get(); + return data->offset_mapping.get(); } // Depth-first-scan of all LayoutInline and LayoutText nodes that make up this // NGInlineNode object. Collects LayoutText items, merging them up into the // 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) { - DCHECK(data->text_content_.IsNull()); - DCHECK(data->items_.IsEmpty()); +void NGInlineNode::CollectInlines(NGInlineNodeData* data, + NGInlineNodeData* previous_data) { + DCHECK(data->text_content.IsNull()); + DCHECK(data->items.IsEmpty()); LayoutBlockFlow* block = GetLayoutBlockFlow(); block->WillCollectInlines(); - NGInlineItemsBuilder builder(&data->items_); - CollectInlinesInternal(block, &builder); - data->text_content_ = builder.ToString(); + + String* previous_text = + previous_data ? &previous_data->text_content : nullptr; + NGInlineItemsBuilder builder(&data->items); + CollectInlinesInternal(block, &builder, previous_text); + data->text_content = builder.ToString(); + // Set |is_bidi_enabled_| for all UTF-16 strings for now, because at this // point the string may or may not contain RTL characters. // |SegmentText()| will analyze the text and reset |is_bidi_enabled_| if it // doesn't contain any RTL characters. data->is_bidi_enabled_ = - !data->text_content_.Is8Bit() || builder.HasBidiControls(); + !data->text_content.Is8Bit() || builder.HasBidiControls(); data->is_empty_inline_ = builder.IsEmptyInline(); } @@ -277,8 +332,8 @@ void NGInlineNode::SegmentText(NGInlineNodeData* data) { } NGBidiParagraph bidi; - data->text_content_.Ensure16Bit(); - if (!bidi.SetParagraph(data->text_content_, Style())) { + data->text_content.Ensure16Bit(); + if (!bidi.SetParagraph(data->text_content, Style())) { // On failure, give up bidi resolving and reordering. data->is_bidi_enabled_ = false; data->SetBaseDirection(TextDirection::kLtr); @@ -293,9 +348,9 @@ void NGInlineNode::SegmentText(NGInlineNodeData* data) { return; } - Vector<NGInlineItem>& items = data->items_; + Vector<NGInlineItem>& items = data->items; unsigned item_index = 0; - for (unsigned start = 0; start < data->text_content_.length();) { + for (unsigned start = 0; start < data->text_content.length();) { UBiDiLevel level; unsigned end = bidi.GetLogicalRun(start, &level); DCHECK_EQ(items[item_index].start_offset_, start); @@ -313,19 +368,21 @@ void NGInlineNode::SegmentText(NGInlineNodeData* data) { #endif } -void NGInlineNode::ShapeText(NGInlineNodeData* data) { +void NGInlineNode::ShapeText(NGInlineItemsData* data, + NGInlineItemsData* previous_data) { // TODO(eae): Add support for shaping latin-1 text? - data->text_content_.Ensure16Bit(); - ShapeText(data->text_content_, &data->items_); - - ShapeTextForFirstLineIfNeeded(data); + data->text_content.Ensure16Bit(); + ShapeText(data->text_content, &data->items, + previous_data ? &previous_data->text_content : nullptr); } void NGInlineNode::ShapeText(const String& text_content, - Vector<NGInlineItem>* items) { - // Shape each item with the full context of the entire node. + Vector<NGInlineItem>* items, + const String* previous_text) { + // Provide full context of the entire node to the shaper. HarfBuzzShaper shaper(text_content.Characters16(), text_content.length()); ShapeResultSpacing<String> spacing(text_content); + for (unsigned index = 0; index < items->size();) { NGInlineItem& start_item = (*items)[index]; if (start_item.Type() != NGInlineItem::kText) { @@ -339,6 +396,12 @@ void NGInlineNode::ShapeText(const String& text_content, unsigned end_offset = start_item.EndOffset(); for (; end_index < items->size(); end_index++) { const NGInlineItem& item = (*items)[end_index]; + + if (item.Type() == NGInlineItem::kControl) { + // Do not shape across control characters (line breaks, zero width + // spaces, etc). + break; + } if (item.Type() == NGInlineItem::kText) { // Shape adjacent items together if the font and direction matches to // allow ligatures and kerning to apply. @@ -359,6 +422,43 @@ void NGInlineNode::ShapeText(const String& text_content, } } + // Shaping a single item. Skip if the existing results remain valid. + if (previous_text && end_offset == start_item.EndOffset() && + !NeedsShaping(start_item)) { + DCHECK_EQ(start_item.StartOffset(), + start_item.TextShapeResult()->StartIndexForResult()); + DCHECK_EQ(start_item.EndOffset(), + start_item.TextShapeResult()->EndIndexForResult()); + index++; + continue; + } + + // Results may only be reused if all items in the range remain valid. + bool has_valid_shape_results = true; + for (unsigned item_index = index; item_index < end_index; item_index++) { + if (NeedsShaping((*items)[item_index])) { + has_valid_shape_results = false; + break; + } + } + + // When shaping across multiple items checking whether the individual + // items has valid shape results isn't sufficient as items may have been + // re-ordered or removed. + // TODO(layout-dev): It would probably be faster to check for removed or + // moved items but for now comparing the string itself will do. + unsigned text_start = start_item.StartOffset(); + DCHECK_GE(end_offset, text_start); + unsigned text_length = end_offset - text_start; + if (has_valid_shape_results && previous_text && + end_offset <= previous_text->length() && + StringView(text_content, text_start, text_length) == + StringView(*previous_text, text_start, text_length)) { + index = end_index; + continue; + } + + // Shape each item with the full context of the entire node. scoped_refptr<ShapeResult> shape_result = shaper.Shape(&font, direction, start_item.StartOffset(), end_offset); if (UNLIKELY(spacing.SetSpacing(font.GetFontDescription()))) @@ -404,9 +504,28 @@ void NGInlineNode::ShapeTextForFirstLineIfNeeded(NGInlineNodeData* data) { if (block_style == first_line_style) return; - auto first_line_items = std::make_unique<Vector<NGInlineItem>>(); - first_line_items->AppendVector(data->items_); - for (auto& item : *first_line_items) { + auto first_line_items = std::make_unique<NGInlineItemsData>(); + first_line_items->text_content = data->text_content; + bool needs_reshape = false; + if (first_line_style->TextTransform() != block_style->TextTransform()) { + // TODO(kojii): This logic assumes that text-transform is applied only to + // ::first-line, and does not work when the base style has text-transform + // and ::first-line has different text-transform. + first_line_style->ApplyTextTransform(&first_line_items->text_content); + if (first_line_items->text_content != data->text_content) { + // TODO(kojii): When text-transform changes the length, we need to adjust + // offset in NGInlineItem, or re-collect inlines. Other classes such as + // line breaker need to support the scenario too. For now, we force the + // string to be the same length to prevent them from crashing. This may + // result in a missing or a duplicate character if the length changes. + TruncateOrPadText(&first_line_items->text_content, + data->text_content.length()); + needs_reshape = true; + } + } + + first_line_items->items.AppendVector(data->items); + for (auto& item : first_line_items->items) { if (item.style_) { DCHECK(item.layout_object_); item.style_ = item.layout_object_->FirstLineStyle(); @@ -415,15 +534,26 @@ void NGInlineNode::ShapeTextForFirstLineIfNeeded(NGInlineNodeData* data) { } // Re-shape if the font is different. - const Font& font = block_style->GetFont(); - const Font& first_line_font = first_line_style->GetFont(); - if (&font != &first_line_font && font != first_line_font) { - ShapeText(data->text_content_, first_line_items.get()); - } + if (needs_reshape || FirstLineNeedsReshape(*first_line_style, *block_style)) + ShapeText(first_line_items.get()); data->first_line_items_ = std::move(first_line_items); } +void NGInlineNode::AssociateItemsWithInlines(NGInlineNodeData* data) { + LayoutObject* last_object = nullptr; + for (auto& item : data->items) { + LayoutObject* object = item.GetLayoutObject(); + if (object && object->IsLayoutNGText()) { + LayoutNGText* layout_text = ToLayoutNGText(object); + if (object != last_object) + layout_text->ClearInlineItems(); + layout_text->AddInlineItem(&item); + } + last_object = object; + } +} + scoped_refptr<NGLayoutResult> NGInlineNode::Layout( const NGConstraintSpace& constraint_space, NGBreakToken* break_token) { @@ -446,6 +576,7 @@ static LayoutUnit ComputeContentSize(NGInlineNode node, NGConstraintSpaceBuilder(writing_mode, node.InitialContainingBlockSize()) .SetTextDirection(style.Direction()) .SetAvailableSize({available_inline_size, NGSizeIndefinite}) + .SetIsIntermediateLayout(true) .ToConstraintSpace(writing_mode); Vector<NGPositionedFloat> positioned_floats; @@ -454,8 +585,7 @@ static LayoutUnit ComputeContentSize(NGInlineNode node, scoped_refptr<NGInlineBreakToken> break_token; NGLineInfo line_info; NGExclusionSpace empty_exclusion_space; - NGLayoutOpportunity opportunity(NGBfcRect( - NGBfcOffset(), NGBfcOffset(available_inline_size, LayoutUnit::Max()))); + NGLineLayoutOpportunity line_opportunity(available_inline_size); LayoutUnit result; LayoutUnit previous_floats_inline_size = input.float_left_inline_size + input.float_right_inline_size; @@ -463,9 +593,10 @@ static LayoutUnit ComputeContentSize(NGInlineNode node, unpositioned_floats.clear(); NGLineBreaker line_breaker(node, mode, *space, &positioned_floats, - &unpositioned_floats, &empty_exclusion_space, 0u, - break_token.get()); - if (!line_breaker.NextLine(opportunity, &line_info)) + &unpositioned_floats, + nullptr /* container_builder */, + &empty_exclusion_space, 0u, break_token.get()); + if (!line_breaker.NextLine(line_opportunity, &line_info)) break; break_token = line_breaker.CreateBreakToken(line_info, nullptr); @@ -490,14 +621,9 @@ static LayoutUnit ComputeContentSize(NGInlineNode node, NGBlockNode float_node = unpositioned_float->node; const ComputedStyle& float_style = float_node.Style(); - Optional<MinMaxSize> child_minmax; - if (NeedMinMaxSizeForContentContribution(float_style)) { - MinMaxSizeInput zero_input; // Floats don't intrude into floats. - child_minmax = float_node.ComputeMinMaxSize(zero_input); - } - - MinMaxSize child_sizes = - ComputeMinAndMaxContentContribution(float_style, child_minmax); + MinMaxSizeInput zero_input; // Floats don't intrude into floats. + MinMaxSize child_sizes = ComputeMinAndMaxContentContribution( + writing_mode, float_node, zero_input); LayoutUnit child_inline_margins = ComputeMinMaxMargins(style, float_node).InlineSum(); @@ -560,7 +686,7 @@ MinMaxSize NGInlineNode::ComputeMinMaxSize(const MinMaxSizeInput& input) { void NGInlineNode::CheckConsistency() const { #if DCHECK_IS_ON() - const Vector<NGInlineItem>& items = Data().items_; + const Vector<NGInlineItem>& items = Data().items; for (const NGInlineItem& item : items) { DCHECK(!item.GetLayoutObject() || !item.Style() || item.Style() == item.GetLayoutObject()->Style()); 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 333c5b1b092..7685706584d 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 @@ -7,28 +7,19 @@ #include "third_party/blink/renderer/core/core_export.h" #include "third_party/blink/renderer/core/layout/layout_block_flow.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_node_data.h" #include "third_party/blink/renderer/core/layout/ng/ng_layout_input_node.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" namespace blink { -template <typename OffsetMappingBuilder> -class NGInlineItemsBuilderTemplate; - -class EmptyOffsetMappingBuilder; -class LayoutBlockFlow; -struct MinMaxSize; class NGConstraintSpace; class NGInlineItem; -class NGInlineItemRange; -using NGInlineItemsBuilder = - NGInlineItemsBuilderTemplate<EmptyOffsetMappingBuilder>; -struct NGInlineNodeData; class NGLayoutResult; class NGOffsetMapping; class NGInlineNodeLegacy; +struct MinMaxSize; +struct NGInlineItemsData; // Represents an anonymous block box to be laid out, that contains consecutive // inline nodes and their descendants. @@ -55,17 +46,12 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode { MinMaxSize ComputeMinMaxSize(const MinMaxSizeInput&); // Instruct to re-compute |PrepareLayout| on the next layout. - void InvalidatePrepareLayout(); + void InvalidatePrepareLayoutForTest(); - const String& Text() const { return Data().text_content_; } - StringView Text(unsigned start_offset, unsigned end_offset) const { - return StringView(Data().text_content_, start_offset, - end_offset - start_offset); + const NGInlineItemsData& ItemsData(bool is_first_line) const { + return Data().ItemsData(is_first_line); } - const Vector<NGInlineItem>& Items(bool is_first_line = false) const; - NGInlineItemRange Items(unsigned start_index, unsigned end_index); - // Returns the DOM to text content offset mapping of this block. If it is not // computed before, compute and store it in NGInlineNodeData. // This funciton must be called with clean layout. @@ -76,8 +62,10 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode { bool IsEmptyInline() { return EnsureData().is_empty_inline_; } - void AssertOffset(unsigned index, unsigned offset) const; - void AssertEndOffset(unsigned index, unsigned offset) const; + // @return if this node can contain the "first formatted line". + // https://www.w3.org/TR/CSS22/selector.html#first-formatted-line + bool CanContainFirstFormattedLine() const; + void CheckConsistency() const; String ToString() const; @@ -89,11 +77,16 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode { // calling the Layout method. void PrepareLayoutIfNeeded(); - void CollectInlines(NGInlineNodeData*); + void CollectInlines(NGInlineNodeData*, + NGInlineNodeData* previous_data = nullptr); void SegmentText(NGInlineNodeData*); - void ShapeText(NGInlineNodeData*); - void ShapeText(const String&, Vector<NGInlineItem>*); + void ShapeText(NGInlineItemsData*, + NGInlineItemsData* previous_data = nullptr); + void ShapeText(const String& text, + Vector<NGInlineItem>*, + const String* previous_text); void ShapeTextForFirstLineIfNeeded(NGInlineNodeData*); + void AssociateItemsWithInlines(NGInlineNodeData*); NGInlineNodeData* MutableData(); const NGInlineNodeData& Data() const; @@ -103,15 +96,6 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode { friend class NGInlineNodeLegacy; }; -inline void NGInlineNode::AssertOffset(unsigned index, unsigned offset) const { - Data().items_[index].AssertOffset(offset); -} - -inline void NGInlineNode::AssertEndOffset(unsigned index, - unsigned offset) const { - Data().items_[index].AssertEndOffset(offset); -} - DEFINE_TYPE_CASTS(NGInlineNode, NGLayoutInputNode, node, diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.cc index a45f608f7b5..07eaa48884b 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.cc @@ -4,13 +4,6 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node_data.h" -#include "third_party/blink/renderer/core/dom/node.h" -#include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h" -#include "third_party/blink/renderer/core/style/computed_style.h" - namespace blink { -NGInlineNodeData::NGInlineNodeData() = default; -NGInlineNodeData::~NGInlineNodeData() = default; - } // namespace blink 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 2d8efffd2d5..dbf328bc9ea 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 @@ -11,16 +11,14 @@ namespace blink { -class NGOffsetMapping; - // Data which is required for inline nodes. -struct CORE_EXPORT NGInlineNodeData { - // The constructor and destructor can't be implicit or inlined, because they - // need full definition of NGOffsetMapping. - NGInlineNodeData(); - ~NGInlineNodeData(); - +struct CORE_EXPORT NGInlineNodeData : NGInlineItemsData { private: + const NGInlineItemsData& ItemsData(bool is_first_line) const { + return !is_first_line || !first_line_items_ + ? (const NGInlineItemsData&)*this + : *first_line_items_; + } TextDirection BaseDirection() const { return static_cast<TextDirection>(base_direction_); } @@ -33,18 +31,12 @@ struct CORE_EXPORT NGInlineNodeData { friend class NGInlineNodeForTest; friend class NGOffsetMappingTest; - // Text content for all inline items represented by a single NGInlineNode. - // Encoded either as UTF-16 or latin-1 depending on the content. - String text_content_; - Vector<NGInlineItem> items_; - - // |items_| to use for the first line, when the node has :first-line rules. - // Items have different ComputedStyle, and may also have different ShapeResult - // if fonts are different. - std::unique_ptr<Vector<NGInlineItem>> first_line_items_; - - // The DOM to text content offset mapping of this inline node. - std::unique_ptr<NGOffsetMapping> offset_mapping_; + // Items to use for the first line, when the node has :first-line rules. + // + // Items have different ComputedStyle, and may also have different + // text_content and ShapeResult if 'text-transform' is applied or fonts are + // different. + std::unique_ptr<NGInlineItemsData> first_line_items_; unsigned is_bidi_enabled_ : 1; unsigned base_direction_ : 1; // TextDirection 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 ca628714647..f04e5e70b56 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 @@ -22,29 +22,29 @@ class NGInlineNodeForTest : public NGInlineNode { public: using NGInlineNode::NGInlineNode; - std::string Text() const { return Data().text_content_.Utf8().data(); } - Vector<NGInlineItem>& Items() { return MutableData()->items_; } + std::string Text() const { return Data().text_content.Utf8().data(); } + Vector<NGInlineItem>& Items() { return MutableData()->items; } static Vector<NGInlineItem>& Items(NGInlineNodeData& data) { - return data.items_; + return data.items; } void Append(const String& text, const ComputedStyle* style = nullptr, LayoutObject* layout_object = nullptr) { NGInlineNodeData* data = MutableData(); - unsigned start = data->text_content_.length(); - data->text_content_.append(text); - data->items_.push_back(NGInlineItem(NGInlineItem::kText, start, - start + text.length(), style, - layout_object)); + unsigned start = data->text_content.length(); + data->text_content.append(text); + data->items.push_back(NGInlineItem(NGInlineItem::kText, start, + start + text.length(), style, + layout_object)); data->is_empty_inline_ = false; } void Append(UChar character) { NGInlineNodeData* data = MutableData(); - data->text_content_.append(character); - unsigned end = data->text_content_.length(); - data->items_.push_back( + data->text_content.append(character); + unsigned end = data->text_content.length(); + data->items.push_back( NGInlineItem(NGInlineItem::kBidiControl, end - 1, end, nullptr)); data->is_bidi_enabled_ = true; data->is_empty_inline_ = false; @@ -52,8 +52,8 @@ class NGInlineNodeForTest : public NGInlineNode { void ClearText() { NGInlineNodeData* data = MutableData(); - data->text_content_ = String(); - data->items_.clear(); + data->text_content = String(); + data->items.clear(); data->is_empty_inline_ = true; } @@ -93,7 +93,7 @@ class NGInlineNodeTest : public NGLayoutTest { if (!layout_block_flow_) SetupHtml("t", "<div id=t style='font:10px'>test</div>"); NGInlineNodeForTest node(layout_block_flow_); - node.InvalidatePrepareLayout(); + node.InvalidatePrepareLayoutForTest(); return node; } @@ -304,7 +304,6 @@ TEST_F(NGInlineNodeTest, SegmentSplit1To2) { NGInlineNodeForTest node = CreateInlineNode(); node.Append(u"Hello \u05E2\u05D1\u05E8\u05D9\u05EA"); node.SegmentText(); - ASSERT_EQ(2u, node.Items().size()); Vector<NGInlineItem>& items = node.Items(); ASSERT_EQ(2u, items.size()); TEST_ITEM_OFFSET_DIR(items[0], 0u, 6u, TextDirection::kLtr); 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 eeef40c579c..2b7a7e3d58e 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 @@ -5,7 +5,6 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment_builder.h" #include "third_party/blink/renderer/core/layout/ng/exclusions/ng_exclusion_space.h" -#include "third_party/blink/renderer/core/layout/ng/geometry/ng_logical_size.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" @@ -56,6 +55,25 @@ const NGPhysicalFragment* NGLineBoxFragmentBuilder::Child::PhysicalFragment() : fragment.get(); } +NGLineBoxFragmentBuilder::Child* +NGLineBoxFragmentBuilder::ChildList::FirstInFlowChild() { + for (auto& child : *this) { + if (child.HasInFlowFragment()) + return &child; + } + return nullptr; +} + +NGLineBoxFragmentBuilder::Child* +NGLineBoxFragmentBuilder::ChildList::LastInFlowChild() { + for (auto it = rbegin(); it != rend(); it++) { + auto& child = *it; + if (child.HasInFlowFragment()) + return &child; + } + return nullptr; +} + void NGLineBoxFragmentBuilder::ChildList::InsertChild( unsigned index, scoped_refptr<NGLayoutResult> layout_result, @@ -128,28 +146,32 @@ scoped_refptr<NGLayoutResult> NGLineBoxFragmentBuilder::ToLineBoxFragment() { NGPhysicalSize physical_size = Size().ConvertToPhysical(writing_mode); NGPhysicalOffsetRect contents_visual_rect({}, physical_size); + NGPhysicalOffsetRect scrollable_overflow({}, physical_size); for (size_t i = 0; i < children_.size(); ++i) { NGPhysicalFragment* child = children_[i].get(); child->SetOffset(offsets_[i].ConvertToPhysical( writing_mode, Direction(), physical_size, child->Size())); child->PropagateContentsVisualRect(&contents_visual_rect); + NGPhysicalOffsetRect child_scroll_overflow = child->ScrollableOverflow(); + child_scroll_overflow.offset += child->Offset(); + scrollable_overflow.Unite(child_scroll_overflow); } scoped_refptr<NGPhysicalLineBoxFragment> fragment = base::AdoptRef(new NGPhysicalLineBoxFragment( - Style(), physical_size, children_, contents_visual_rect, metrics_, - base_direction_, + Style(), style_variant_, physical_size, children_, + contents_visual_rect, scrollable_overflow, metrics_, base_direction_, break_token_ ? std::move(break_token_) : NGInlineBreakToken::Create(node_))); return base::AdoptRef(new NGLayoutResult( std::move(fragment), oof_positioned_descendants_, positioned_floats_, - unpositioned_floats_, unpositioned_list_marker_, - std::move(exclusion_space_), bfc_offset_, end_margin_strut_, + unpositioned_list_marker_, std::move(exclusion_space_), bfc_offset_, + end_margin_strut_, /* intrinsic_block_size */ LayoutUnit(), /* minimal_space_shortage */ LayoutUnit::Max(), EBreakBetween::kAuto, EBreakBetween::kAuto, /* has_forced_break */ false, is_pushed_by_floats_, - NGLayoutResult::kSuccess)); + adjoining_floats_, NGLayoutResult::kSuccess)); } } // namespace blink 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 9f4ed515a70..9ea29183fb1 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 @@ -9,15 +9,15 @@ #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/ng_container_fragment_builder.h" -#include "third_party/blink/renderer/core/layout/ng/ng_positioned_float.h" +#include "third_party/blink/renderer/core/layout/ng/ng_layout_result.h" #include "third_party/blink/renderer/platform/wtf/allocator.h" namespace blink { class ComputedStyle; class NGInlineBreakToken; -class NGInlineNode; class NGPhysicalFragment; +struct NGPositionedFloat; class CORE_EXPORT NGLineBoxFragmentBuilder final : public NGContainerFragmentBuilder { @@ -78,6 +78,14 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final bidi_level(bidi_level) {} // Create an in-flow |NGPhysicalFragment|. Child(scoped_refptr<NGPhysicalFragment> fragment, + NGLogicalOffset offset, + LayoutUnit inline_size, + UBiDiLevel bidi_level) + : fragment(std::move(fragment)), + offset(offset), + inline_size(inline_size), + bidi_level(bidi_level) {} + Child(scoped_refptr<NGPhysicalFragment> fragment, LayoutUnit block_offset, LayoutUnit inline_size, UBiDiLevel bidi_level) @@ -130,6 +138,15 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final using const_iterator = Vector<Child, 16>::const_iterator; const_iterator begin() const { return children_.begin(); } const_iterator end() const { return children_.end(); } + using reverse_iterator = Vector<Child, 16>::reverse_iterator; + reverse_iterator rbegin() { return children_.rbegin(); } + reverse_iterator rend() { return children_.rend(); } + using const_reverse_iterator = Vector<Child, 16>::const_reverse_iterator; + const_reverse_iterator rbegin() const { return children_.rbegin(); } + const_reverse_iterator rend() const { return children_.rend(); } + + Child* FirstInFlowChild(); + Child* LastInFlowChild(); // Add a child. Accepts all constructor arguments for |Child|. template <class... Args> 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 343a5fec56c..79294415634 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 @@ -8,12 +8,12 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.h" -#include "third_party/blink/renderer/core/layout/ng/ng_box_fragment.h" #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space.h" -#include "third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h" #include "third_party/blink/renderer/core/layout/ng/ng_floats_utils.h" +#include "third_party/blink/renderer/core/layout/ng/ng_fragment.h" #include "third_party/blink/renderer/core/layout/ng/ng_length_utils.h" #include "third_party/blink/renderer/core/layout/ng/ng_positioned_float.h" +#include "third_party/blink/renderer/core/layout/ng/ng_unpositioned_float.h" #include "third_party/blink/renderer/core/style/computed_style.h" #include "third_party/blink/renderer/platform/fonts/shaping/shaping_line_breaker.h" @@ -35,24 +35,40 @@ inline bool CanBreakAfterLast(const NGInlineItemResults& item_results) { } // namespace +NGLineBreaker::LineData::LineData(NGInlineNode node, + const NGInlineBreakToken* break_token) { + is_first_formatted_line = (!break_token || (!break_token->ItemIndex() && + !break_token->TextOffset())) && + node.CanContainFirstFormattedLine(); + use_first_line_style = is_first_formatted_line && node.GetLayoutObject() + ->GetDocument() + .GetStyleEngine() + .UsesFirstLineRules(); +} + NGLineBreaker::NGLineBreaker( NGInlineNode node, NGLineBreakerMode mode, const NGConstraintSpace& space, Vector<NGPositionedFloat>* positioned_floats, Vector<scoped_refptr<NGUnpositionedFloat>>* unpositioned_floats, + NGContainerFragmentBuilder* container_builder, NGExclusionSpace* exclusion_space, unsigned handled_float_index, const NGInlineBreakToken* break_token) - : node_(node), + : line_(node, break_token), + node_(node), + items_data_(node.ItemsData(line_.use_first_line_style)), mode_(mode), constraint_space_(space), positioned_floats_(positioned_floats), unpositioned_floats_(unpositioned_floats), + container_builder_(container_builder), exclusion_space_(exclusion_space), - break_iterator_(node.Text()), - shaper_(node.Text().Characters16(), node.Text().length()), - spacing_(node.Text()), + break_iterator_(items_data_.text_content), + shaper_(items_data_.text_content.Characters16(), + items_data_.text_content.length()), + spacing_(items_data_.text_content), handled_floats_end_item_index_(handled_float_index), base_direction_(node_.BaseDirection()), in_line_height_quirks_mode_(node.InLineHeightQuirksMode()) { @@ -63,11 +79,15 @@ NGLineBreaker::NGLineBreaker( item_index_ = break_token->ItemIndex(); offset_ = break_token->TextOffset(); previous_line_had_forced_break_ = break_token->IsForcedBreak(); - node.AssertOffset(item_index_, offset_); + items_data_.AssertOffset(item_index_, offset_); ignore_floats_ = break_token->IgnoreFloats(); } } +// Define the destructor here, so that we can forward-declare more in the +// header. +NGLineBreaker::~NGLineBreaker() = default; + inline NGInlineItemResult* NGLineBreaker::AddItem( const NGInlineItem& item, unsigned end_offset, @@ -106,59 +126,53 @@ inline void NGLineBreaker::ComputeCanBreakAfter( auto_wrap_ && break_iterator_.IsBreakable(item_result->end_offset); } -// @return if this is the "first formatted line". -// https://www.w3.org/TR/CSS22/selector.html#first-formatted-line -bool NGLineBreaker::IsFirstFormattedLine() const { - if (item_index_ || offset_) - return false; - - // TODO(kojii): In LayoutNG, leading OOF creates an anonymous block box, - // and that |CanContainFirstFormattedLine()| does not work. - // crbug.com/734554 - // return node_.GetLayoutBlockFlow()->CanContainFirstFormattedLine(); - LayoutObject* layout_object = node_.GetLayoutBlockFlow(); - if (!layout_object->IsAnonymousBlock()) - return true; - for (;;) { - layout_object = layout_object->PreviousSibling(); - if (!layout_object) - return true; - if (!layout_object->IsFloatingOrOutOfFlowPositioned()) +// True if |item| is trailing; i.e., |item| and all items after it are opaque to +// whitespace collapsing. +bool NGLineBreaker::IsTrailing(const NGInlineItem& item, + const NGLineInfo& line_info) const { + const Vector<NGInlineItem>& items = line_info.ItemsData().items; + for (const NGInlineItem* it = &item; it != items.end(); ++it) { + if (it->EndCollapseType() != NGInlineItem::kOpaqueToCollapsing) return false; } + return true; } // Compute the base direction for bidi algorithm for this line. -void NGLineBreaker::ComputeBaseDirection() { +void NGLineBreaker::ComputeBaseDirection(const NGLineInfo& line_info) { // If 'unicode-bidi' is not 'plaintext', use the base direction of the block. if (!previous_line_had_forced_break_ || node_.Style().GetUnicodeBidi() != UnicodeBidi::kPlaintext) return; // If 'unicode-bidi: plaintext', compute the base direction for each paragraph // (separated by forced break.) - const String& text = node_.Text(); + const String& text = line_info.ItemsData().text_content; if (text.Is8Bit()) return; size_t end_offset = text.find(kNewlineCharacter, offset_); - base_direction_ = NGBidiParagraph::BaseDirectionForString(node_.Text( - offset_, end_offset == kNotFound ? text.length() : end_offset)); + base_direction_ = NGBidiParagraph::BaseDirectionForString( + end_offset == kNotFound + ? StringView(text, offset_) + : StringView(text, offset_, end_offset - offset_)); } // Initialize internal states for the next line. -void NGLineBreaker::PrepareNextLine(const NGLayoutOpportunity& opportunity, - NGLineInfo* line_info) { +void NGLineBreaker::PrepareNextLine( + const NGLineLayoutOpportunity& line_opportunity, + NGLineInfo* line_info) { NGInlineItemResults* item_results = &line_info->Results(); item_results->clear(); line_info->SetStartOffset(offset_); - line_info->SetLineStyle(node_, constraint_space_, IsFirstFormattedLine(), - previous_line_had_forced_break_); + line_info->SetLineStyle( + node_, items_data_, constraint_space_, line_.is_first_formatted_line, + line_.use_first_line_style, previous_line_had_forced_break_); // Set the initial style of this line from the break token. Example: // <p>...<span>....</span></p> // When the line wraps in <span>, the 2nd line needs to start with the style // of the <span>. override_break_anywhere_ = false; SetCurrentStyle(current_style_ ? *current_style_ : line_info->LineStyle()); - ComputeBaseDirection(); + ComputeBaseDirection(*line_info); line_info->SetBaseDirection(base_direction_); line_.is_after_forced_break = false; @@ -168,17 +182,12 @@ void NGLineBreaker::PrepareNextLine(const NGLayoutOpportunity& opportunity, // regardless of 'text-indent'. line_.position = line_info->TextIndent(); - line_.opportunity = opportunity; - line_.line_left_bfc_offset = opportunity.rect.LineStartOffset(); - line_.line_right_bfc_offset = opportunity.rect.LineEndOffset(); - bfc_block_offset_ = opportunity.rect.BlockStartOffset(); + line_.line_opportunity = line_opportunity; } -bool NGLineBreaker::NextLine(const NGLayoutOpportunity& opportunity, +bool NGLineBreaker::NextLine(const NGLineLayoutOpportunity& line_opportunity, NGLineInfo* line_info) { - bfc_block_offset_ = constraint_space_.BfcOffset().block_offset; - - PrepareNextLine(opportunity, line_info); + PrepareNextLine(line_opportunity, line_info); BreakLine(line_info); if (line_info->Results().IsEmpty()) @@ -186,22 +195,15 @@ bool NGLineBreaker::NextLine(const NGLayoutOpportunity& opportunity, // TODO(kojii): There are cases where we need to PlaceItems() without creating // line boxes. These cases need to be reviewed. - if (line_.should_create_line_box) { - if (!line_.CanFit() && - node_.GetLayoutBlockFlow()->ShouldTruncateOverflowingText()) { - TruncateOverflowingText(line_info); - } - + if (line_.should_create_line_box) ComputeLineLocation(line_info); - } return true; } void NGLineBreaker::BreakLine(NGLineInfo* line_info) { NGInlineItemResults* item_results = &line_info->Results(); - const Vector<NGInlineItem>& items = - node_.Items(line_info->UseFirstLineStyle()); + const Vector<NGInlineItem>& items = line_info->ItemsData().items; LineBreakState state = LineBreakState::kContinue; while (state != LineBreakState::kDone) { // Check overflow even if |item_index_| is at the end of the block, because @@ -214,6 +216,7 @@ void NGLineBreaker::BreakLine(NGLineInfo* line_info) { // If we reach at the end of the block, this is the last line. DCHECK_LE(item_index_, items.size()); if (item_index_ == items.size()) { + RemoveTrailingCollapsibleSpace(line_info); line_info->SetIsLastLine(true); return; } @@ -255,7 +258,7 @@ void NGLineBreaker::BreakLine(NGLineInfo* line_info) { } else if (item.Type() == NGInlineItem::kOpenTag) { HandleOpenTag(item, AddItem(item, item_results)); } else if (item.Type() == NGInlineItem::kFloating) { - HandleFloat(item, AddItem(item, item_results)); + HandleFloat(item, line_info, AddItem(item, item_results)); } else if (item.Type() == NGInlineItem::kOutOfFlowPositioned) { DCHECK_EQ(item.Length(), 0u); AddItem(item, item_results); @@ -291,14 +294,14 @@ void NGLineBreaker::UpdatePosition(const NGInlineItemResults& results) { } void NGLineBreaker::ComputeLineLocation(NGLineInfo* line_info) const { - LayoutUnit bfc_line_offset = line_.line_left_bfc_offset; + LayoutUnit bfc_line_offset = line_.line_opportunity.line_left_offset; LayoutUnit available_width = line_.AvailableWidth(); // Negative margins can make the position negative, but the inline size is // always positive or 0. - line_info->SetLineBfcOffset({bfc_line_offset, bfc_block_offset_}, - available_width, - line_.position.ClampNegativeToZero()); + line_info->SetLineBfcOffset( + {bfc_line_offset, line_.line_opportunity.bfc_block_offset}, + available_width, line_.position.ClampNegativeToZero()); } NGLineBreaker::LineBreakState NGLineBreaker::HandleText( @@ -394,7 +397,7 @@ void NGLineBreaker::BreakText(NGInlineItemResult* item_result, // * If offset == item.EndOffset(): the break opportunity at the end fits, // or the first break opportunity is beyond the end. // There may be room for more characters. - // * If width > available_width: The first break opporunity does not fit. + // * If width > available_width: The first break opportunity does not fit. // offset is the first break opportunity, either inside, at the end, or // beyond the end. if (item_result->end_offset < item.EndOffset()) { @@ -438,6 +441,7 @@ NGLineBreaker::LineBreakState NGLineBreaker::HandleTrailingSpaces( NGInlineItemResult* item_result = AddItem(item, end, item_results); item_result->has_only_trailing_spaces = true; + // TODO(kojii): Should reshape if it's not safe to break. item_result->shape_result = item.TextShapeResult()->SubRange(offset_, end); item_result->inline_size = item_result->shape_result->SnappedWidth(); line_.position += item_result->inline_size; @@ -455,6 +459,44 @@ NGLineBreaker::LineBreakState NGLineBreaker::HandleTrailingSpaces( return LineBreakState::kTrailing; } +// Remove trailing collapsible spaces in |line_info|. +// https://drafts.csswg.org/css-text-3/#white-space-phase-2 +void NGLineBreaker::RemoveTrailingCollapsibleSpace(NGLineInfo* line_info) { + NGInlineItemResults* item_results = &line_info->Results(); + if (item_results->IsEmpty()) + return; + for (auto it = item_results->rbegin(); it != item_results->rend(); ++it) { + NGInlineItemResult& item_result = *it; + DCHECK(item_result.item); + const NGInlineItem& item = *item_result.item; + if (item.EndCollapseType() == NGInlineItem::kOpaqueToCollapsing) + continue; + if (item.Type() != NGInlineItem::kText) + return; + const String& text = Text(); + if (text[item_result.end_offset - 1] != kSpaceCharacter) + return; + DCHECK(item.Style()); + if (!item.Style()->CollapseWhiteSpace()) + return; + + // We have a trailing collapsible space. Remove it. + line_.position -= item_result.inline_size; + --item_result.end_offset; + if (item_result.end_offset == item_result.start_offset) { + unsigned index = std::distance(item_results->begin(), &item_result); + item_results->EraseAt(index); + } else { + // TODO(kojii): Should reshape if it's not safe to break. + item_result.shape_result = item_result.shape_result->SubRange( + item_result.start_offset, item_result.end_offset); + item_result.inline_size = item_result.shape_result->SnappedWidth(); + line_.position += item_result.inline_size; + } + return; + } +} + void NGLineBreaker::AppendHyphen(const NGInlineItem& item, NGLineInfo* line_info) { DCHECK(item.Style()); @@ -565,16 +607,33 @@ void NGLineBreaker::HandleAtomicInline(const NGInlineItem& item, line_.should_create_line_box = true; NGInlineItemResult* item_result = AddItem(item, &line_info->Results()); - item_result->layout_result = - NGBlockNode(ToLayoutBox(item.GetLayoutObject())) - .LayoutAtomicInline(constraint_space_, - line_info->UseFirstLineStyle()); - DCHECK(item_result->layout_result->PhysicalFragment()); - - item_result->inline_size = - NGFragment(constraint_space_.GetWritingMode(), - *item_result->layout_result->PhysicalFragment()) - .InlineSize(); + // When we're just computing min/max content sizes, we can skip the full + // layout and just compute those sizes. On the other hand, for regular + // layout we need to do the full layout and get the layout result. + // Doing a full layout for min/max content can also have undesirable + // side effects when that falls back to legacy layout. + if (mode_ == NGLineBreakerMode::kContent) { + item_result->layout_result = + NGBlockNode(ToLayoutBox(item.GetLayoutObject())) + .LayoutAtomicInline(constraint_space_, + 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()) + .InlineSize(); + } else { + NGBlockNode block_node(ToLayoutBox(item.GetLayoutObject())); + MinMaxSizeInput input; + MinMaxSize sizes = ComputeMinAndMaxContentContribution( + constraint_space_.GetWritingMode(), block_node, input, + &constraint_space_); + item_result->inline_size = mode_ == NGLineBreakerMode::kMinContent + ? sizes.min_size + : sizes.max_size; + } DCHECK(item.Style()); item_result->margins = @@ -601,9 +660,8 @@ void NGLineBreaker::HandleAtomicInline(const NGInlineItem& item, // We have this check if there are already UnpositionedFloats as we aren't // allowed to position a float "above" another float which has come before us // in the document. -// -// TODO(glebl): Add the support of clearance for inline floats. void NGLineBreaker::HandleFloat(const NGInlineItem& item, + NGLineInfo* line_info, NGInlineItemResult* item_result) { // When rewind occurs, an item may be handled multiple times. // Since floats are put into a separate list, avoid handling same floats @@ -619,6 +677,13 @@ void NGLineBreaker::HandleFloat(const NGInlineItem& item, if (item_index_ <= handled_floats_end_item_index_ || ignore_floats_) return; + // Floats need to know the current line width to determine whether to put it + // into the current line or to the next line. Remove trailing spaces if this + // float is trailing, because whitespace should be collapsed across floats, + // and this logic requires the width after trailing spaces are collapsed. + if (IsTrailing(item, *line_info)) + RemoveTrailingCollapsibleSpace(line_info); + NGBlockNode node(ToLayoutBox(item.GetLayoutObject())); const ComputedStyle& float_style = node.Style(); @@ -641,49 +706,47 @@ void NGLineBreaker::HandleFloat(const NGInlineItem& item, margins.InlineSum()) .ClampNegativeToZero(); + LayoutUnit bfc_block_offset = line_.line_opportunity.bfc_block_offset; + // The float should be positioned after the current line if: - // - It can't fit. + // - It can't fit within the non-shape area. (Assuming the current position + // also is strictly within the non-shape area). // - It will be moved down due to block-start edge alignment. // - It will be moved down due to clearance. // - We are currently computing our min/max-content size. (We use the // unpositioned_floats to manually adjust the min/max-content size after // the line breaker has run). bool float_after_line = - !line_.CanFit(inline_margin_size) || - exclusion_space_->LastFloatBlockStart() > bfc_block_offset_ || + !line_.CanFloatFit(inline_margin_size) || + exclusion_space_->LastFloatBlockStart() > bfc_block_offset || exclusion_space_->ClearanceOffset(float_style.Clear()) > - bfc_block_offset_ || + bfc_block_offset || mode_ != NGLineBreakerMode::kContent; // Check if we already have a pending float. That's because a float cannot be // higher than any block or floated box generated before. if (!unpositioned_floats_->IsEmpty() || float_after_line) { - unpositioned_floats_->push_back(std::move(unpositioned_float)); + AddUnpositionedFloat(unpositioned_floats_, container_builder_, + std::move(unpositioned_float)); } else { - LayoutUnit origin_block_offset = bfc_block_offset_; - NGPositionedFloat positioned_float = PositionFloat( - origin_block_offset, constraint_space_.BfcOffset().block_offset, + bfc_block_offset, constraint_space_.BfcOffset().block_offset, unpositioned_float.get(), constraint_space_, exclusion_space_); positioned_floats_->push_back(positioned_float); DCHECK_EQ(positioned_float.bfc_offset.block_offset, - bfc_block_offset_ + margins.block_start); + bfc_block_offset + margins.block_start); - if (float_style.Floating() == EFloat::kLeft) { - line_.line_left_bfc_offset = std::max( - line_.line_left_bfc_offset, - positioned_float.bfc_offset.line_offset + inline_margin_size - - margins.LineLeft(TextDirection::kLtr)); - } else { - line_.line_right_bfc_offset = - std::min(line_.line_right_bfc_offset, - positioned_float.bfc_offset.line_offset - - margins.LineLeft(TextDirection::kLtr)); - } + NGLayoutOpportunity opportunity = exclusion_space_->FindLayoutOpportunity( + {constraint_space_.BfcOffset().line_offset, bfc_block_offset}, + constraint_space_.AvailableSize().inline_size, NGLogicalSize()); + + DCHECK_EQ(bfc_block_offset, opportunity.rect.BlockStartOffset()); + + line_.line_opportunity = opportunity.ComputeLineLayoutOpportunity( + constraint_space_, line_.line_opportunity.line_block_size, + LayoutUnit()); - DCHECK_GE(line_.line_left_bfc_offset, LayoutUnit()); - DCHECK_GE(line_.line_right_bfc_offset, LayoutUnit()); DCHECK_GE(line_.AvailableWidth(), LayoutUnit()); } } @@ -783,7 +846,7 @@ void NGLineBreaker::HandleCloseTag(const NGInlineItem& item, // be a break opportunity after the space. The break_iterator cannot // compute this because it considers break opportunities are before a run // of spaces. - const String& text = node_.Text(); + const String& text = Text(); if (offset_ < text.length() && IsBreakableSpace(text[offset_])) { item_result->can_break_after = true; return; @@ -858,7 +921,7 @@ NGLineBreaker::LineBreakState NGLineBreaker::HandleOverflow( #endif item_index_ = item_result->item_index; offset_ = item_result->end_offset; - node_.AssertOffset(item_index_, offset_); + items_data_.AssertOffset(item_index_, offset_); } else { Rewind(line_info, i + 1); } @@ -879,7 +942,7 @@ NGLineBreaker::LineBreakState NGLineBreaker::HandleOverflow( } // Let this line overflow. - // If there was a break opporunity, the overflow should stop there. + // If there was a break opportunity, the overflow should stop there. if (break_before) { Rewind(line_info, break_before); return LineBreakState::kTrailing; @@ -918,8 +981,7 @@ void NGLineBreaker::Rewind(NGLineInfo* line_info, unsigned new_end) { // paint invalidations, hit testing, etc. LayoutObject* NGLineBreaker::CurrentLayoutObject( const NGLineInfo& line_info) const { - const Vector<NGInlineItem>& items = - node_.Items(line_info.UseFirstLineStyle()); + const Vector<NGInlineItem>& items = line_info.ItemsData().items; DCHECK_LE(item_index_, items.size()); // Find the next item that has LayoutObject. Some items such as bidi controls // do not have LayoutObject. @@ -936,56 +998,6 @@ LayoutObject* NGLineBreaker::CurrentLayoutObject( return nullptr; } -// Truncate overflowing text and append ellipsis. -void NGLineBreaker::TruncateOverflowingText(NGLineInfo* line_info) { - // The ellipsis is styled according to the line style. - const Font& font = line_info->LineStyle().GetFont(); - const SimpleFontData* font_data = font.PrimaryFont(); - DCHECK(font_data); - String ellipsis = - font_data && font_data->GlyphForCharacter(kHorizontalEllipsisCharacter) - ? String(&kHorizontalEllipsisCharacter, 1) - : String(u"..."); - HarfBuzzShaper shaper(ellipsis.Characters16(), ellipsis.length()); - scoped_refptr<ShapeResult> shape_result = - shaper.Shape(&font, line_info->BaseDirection()); - - // Truncate the line to (available_width - ellipsis_width) using 'line-break: - // anywhere'. - unsigned saved_item_index = item_index_; - unsigned saved_offset = offset_; - override_break_anywhere_ = true; - break_iterator_.SetBreakType(LineBreakType::kBreakCharacter); - HandleOverflow(line_info, - line_.AvailableWidth() - shape_result->SnappedWidth()); - - // Find the LayoutObject this ellpsis is tied to. - LayoutObject* layout_object = CurrentLayoutObject(*line_info); - - // Restore item_index/offset to before HandleOverflow(). - item_index_ = saved_item_index; - offset_ = saved_offset; - - // Ellipsis should not have text decorations. Reset if it's set. - scoped_refptr<const ComputedStyle> style = &line_info->LineStyle(); - if (style->TextDecorationsInEffect() != TextDecoration::kNone) { - scoped_refptr<ComputedStyle> ellipsis_style = - ComputedStyle::CreateAnonymousStyleWithDisplay(*style, - EDisplay::kInline); - ellipsis_style->ResetTextDecoration(); - ellipsis_style->ClearAppliedTextDecorations(); - style = std::move(ellipsis_style); - } - - // The ellipsis should appear at the logical end of the line. - // This is stored seprately from other results so that it can be appended - // after bidi reorder. - NGTextFragmentBuilder builder(node_, constraint_space_.GetWritingMode()); - builder.SetText(layout_object, ellipsis, style, true /* is_ellipsis_style */, - std::move(shape_result)); - SetLineEndFragment(builder.ToTextFragment(), line_info); -} - void NGLineBreaker::SetCurrentStyle(const ComputedStyle& style) { current_style_ = &style; @@ -1041,7 +1053,7 @@ void NGLineBreaker::MoveToNextOf(const NGInlineItemResult& item_result) { scoped_refptr<NGInlineBreakToken> NGLineBreaker::CreateBreakToken( const NGLineInfo& line_info, std::unique_ptr<const NGInlineLayoutStateStack> state_stack) const { - const Vector<NGInlineItem>& items = node_.Items(); + const Vector<NGInlineItem>& items = Items(); if (item_index_ >= items.size()) return NGInlineBreakToken::Create(node_); return NGInlineBreakToken::Create( 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 98e15a95b37..b0838c40a67 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 @@ -6,7 +6,7 @@ #define NGLineBreaker_h #include "third_party/blink/renderer/core/core_export.h" -#include "third_party/blink/renderer/core/layout/ng/exclusions/ng_layout_opportunity.h" +#include "third_party/blink/renderer/core/layout/ng/exclusions/ng_line_layout_opportunity.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_item_result.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h" #include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h" @@ -17,10 +17,12 @@ namespace blink { class Hyphenation; +class NGContainerFragmentBuilder; class NGInlineBreakToken; class NGInlineItem; class NGInlineLayoutStateStack; struct NGPositionedFloat; +struct NGUnpositionedFloat; // The line breaker needs to know which mode its in to properly handle floats. enum class NGLineBreakerMode { kContent, kMinContent, kMaxContent }; @@ -38,14 +40,15 @@ class CORE_EXPORT NGLineBreaker { const NGConstraintSpace&, Vector<NGPositionedFloat>*, Vector<scoped_refptr<NGUnpositionedFloat>>*, + NGContainerFragmentBuilder* container_builder, NGExclusionSpace*, unsigned handled_float_index, const NGInlineBreakToken* = nullptr); - ~NGLineBreaker() = default; + ~NGLineBreaker(); // Compute the next line break point and produces NGInlineItemResults for // the line. - bool NextLine(const NGLayoutOpportunity&, NGLineInfo*); + bool NextLine(const NGLineLayoutOpportunity& line_opportunity, NGLineInfo*); // Create an NGInlineBreakToken for the last line returned by NextLine(). scoped_refptr<NGInlineBreakToken> CreateBreakToken( @@ -63,15 +66,19 @@ class CORE_EXPORT NGLineBreaker { struct LineData { STACK_ALLOCATED(); + LineData(NGInlineNode node, const NGInlineBreakToken* break_token); + // The current position from inline_start. Unlike NGInlineLayoutAlgorithm // that computes position in visual order, this position in logical order. LayoutUnit position; - // The current opportunity. - NGLayoutOpportunity opportunity; + NGLineLayoutOpportunity line_opportunity; + + // True if this line is the "first formatted line". + // https://www.w3.org/TR/CSS22/selector.html#first-formatted-line + bool is_first_formatted_line; - LayoutUnit line_left_bfc_offset; - LayoutUnit line_right_bfc_offset; + bool use_first_line_style; // We don't create "certain zero-height line boxes". // https://drafts.csswg.org/css2/visuren.html#phantom-line-box @@ -85,16 +92,20 @@ class CORE_EXPORT NGLineBreaker { bool is_after_forced_break = false; LayoutUnit AvailableWidth() const { - DCHECK_GE(line_right_bfc_offset, line_left_bfc_offset); - return line_right_bfc_offset - line_left_bfc_offset; + return line_opportunity.AvailableInlineSize(); } bool CanFit() const { return position <= AvailableWidth(); } bool CanFit(LayoutUnit extra) const { return position + extra <= AvailableWidth(); } + bool CanFloatFit(LayoutUnit extra) const { + return position + extra <= line_opportunity.AvailableFloatInlineSize(); + } }; - const String& Text() const { return break_iterator_.GetString(); } + const String& Text() const { return items_data_.text_content; } + const Vector<NGInlineItem>& Items() const { return items_data_.items; } + NGInlineItemResult* AddItem(const NGInlineItem&, unsigned end_offset, NGInlineItemResults*); @@ -104,7 +115,7 @@ class CORE_EXPORT NGLineBreaker { void BreakLine(NGLineInfo*); - void PrepareNextLine(const NGLayoutOpportunity&, NGLineInfo*); + void PrepareNextLine(const NGLineLayoutOpportunity&, NGLineInfo*); void UpdatePosition(const NGInlineItemResults&); void ComputeLineLocation(NGLineInfo*) const; @@ -128,6 +139,7 @@ class CORE_EXPORT NGLineBreaker { LayoutUnit available_width, NGLineInfo*); LineBreakState HandleTrailingSpaces(const NGInlineItem&, NGLineInfo*); + void RemoveTrailingCollapsibleSpace(NGLineInfo*); void AppendHyphen(const NGInlineItem& item, NGLineInfo*); LineBreakState HandleControlItem(const NGInlineItem&, @@ -137,7 +149,7 @@ class CORE_EXPORT NGLineBreaker { LineBreakState, NGLineInfo*); void HandleAtomicInline(const NGInlineItem&, NGLineInfo*); - void HandleFloat(const NGInlineItem&, NGInlineItemResult*); + void HandleFloat(const NGInlineItem&, NGLineInfo*, NGInlineItemResult*); void HandleOpenTag(const NGInlineItem&, NGInlineItemResult*); void HandleCloseTag(const NGInlineItem&, NGInlineItemResults*); @@ -147,33 +159,33 @@ class CORE_EXPORT NGLineBreaker { void Rewind(NGLineInfo*, unsigned new_end); LayoutObject* CurrentLayoutObject(const NGLineInfo&) const; - void TruncateOverflowingText(NGLineInfo*); void SetCurrentStyle(const ComputedStyle&); void MoveToNextOf(const NGInlineItem&); void MoveToNextOf(const NGInlineItemResult&); - bool IsFirstFormattedLine() const; - void ComputeBaseDirection(); + void ComputeBaseDirection(const NGLineInfo&); + bool IsTrailing(const NGInlineItem&, const NGLineInfo&) const; LineData line_; NGInlineNode node_; + const NGInlineItemsData& items_data_; + NGLineBreakerMode mode_; const NGConstraintSpace& constraint_space_; Vector<NGPositionedFloat>* positioned_floats_; Vector<scoped_refptr<NGUnpositionedFloat>>* unpositioned_floats_; + NGContainerFragmentBuilder* container_builder_; /* May be nullptr */ NGExclusionSpace* exclusion_space_; scoped_refptr<const ComputedStyle> current_style_; unsigned item_index_ = 0; unsigned offset_ = 0; - bool previous_line_had_forced_break_ = false; - LayoutUnit bfc_line_offset_; - LayoutUnit bfc_block_offset_; LazyLineBreakIterator break_iterator_; HarfBuzzShaper shaper_; ShapeResultSpacing<String> spacing_; + bool previous_line_had_forced_break_ = false; const Hyphenation* hyphenation_ = nullptr; // Keep track of handled float items. See HandleFloat(). 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 9cf39096049..30c5402b812 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 @@ -11,6 +11,7 @@ #include "third_party/blink/renderer/core/layout/ng/ng_constraint_space_builder.h" #include "third_party/blink/renderer/core/layout/ng/ng_fragment_builder.h" #include "third_party/blink/renderer/core/layout/ng/ng_positioned_float.h" +#include "third_party/blink/renderer/core/layout/ng/ng_unpositioned_float.h" #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" namespace blink { @@ -46,15 +47,14 @@ class NGLineBreakerTest : public NGBaseLayoutAlgorithmTest { Vector<NGInlineItemResults> lines; NGExclusionSpace exclusion_space; - NGLayoutOpportunity opportunity; - opportunity.rect = - NGBfcRect(NGBfcOffset(), {available_width, LayoutUnit::Max()}); + NGLineLayoutOpportunity line_opportunity(available_width); NGLineInfo line_info; while (!break_token || !break_token->IsFinished()) { NGLineBreaker line_breaker(node, NGLineBreakerMode::kContent, *space, &positioned_floats, &unpositioned_floats, + /* container_builder */ nullptr, &exclusion_space, 0u, break_token.get()); - if (!line_breaker.NextLine(opportunity, &line_info)) + if (!line_breaker.NextLine(line_opportunity, &line_info)) break; break_token = line_breaker.CreateBreakToken(line_info, nullptr); @@ -69,8 +69,11 @@ namespace { String ToString(NGInlineItemResults line, NGInlineNode node) { StringBuilder builder; + const String& text = node.ItemsData(false).text_content; for (const auto& item_result : line) { - builder.Append(node.Text(item_result.start_offset, item_result.end_offset)); + builder.Append( + StringView(text, item_result.start_offset, + item_result.end_offset - item_result.start_offset)); } return builder.ToString(); } @@ -181,7 +184,7 @@ TEST_F(NGLineBreakerTest, OverflowMargin) { </style> <div id=container><span>123 456</span> 789</div> )HTML"); - const Vector<NGInlineItem>& items = node.Items(); + const Vector<NGInlineItem>& items = node.ItemsData(false).items; // While "123 456" can fit in a line, "456" has a right margin that cannot // fit. Since "456" and its right margin is not breakable, "456" should be on diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.cc index 44d25987ad9..b83d812d685 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.cc @@ -15,6 +15,9 @@ NGLineHeightMetrics::NGLineHeightMetrics(const ComputedStyle& style, Initialize(font_data->GetFontMetrics(), baseline_type); } +NGLineHeightMetrics::NGLineHeightMetrics(const ComputedStyle& style) + : NGLineHeightMetrics(style, style.GetFontBaseline()) {} + NGLineHeightMetrics::NGLineHeightMetrics(const FontMetrics& font_metrics, FontBaseline baseline_type) { Initialize(font_metrics, baseline_type); diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.h index 8c8c57dc515..978c170044e 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.h @@ -25,6 +25,7 @@ struct NGLineHeightMetrics { // Compute from ComputedStyle, using the font metrics of the prikmary font. // The leading is not included. + NGLineHeightMetrics(const ComputedStyle&); NGLineHeightMetrics(const ComputedStyle&, FontBaseline); // Compute from FontMetrics. The leading is not included. 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 new file mode 100644 index 00000000000..ae76600c718 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_truncator.cc @@ -0,0 +1,201 @@ +// 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_line_truncator.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_text_fragment_builder.h" +#include "third_party/blink/renderer/platform/fonts/font_baseline.h" +#include "third_party/blink/renderer/platform/fonts/shaping/harf_buzz_shaper.h" + +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), + line_style_(&line_info.LineStyle()), + available_width_(line_info.AvailableWidth()), + line_direction_(line_info.BaseDirection()) {} + +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_); + const Font& font = ellipsis_style->GetFont(); + const SimpleFontData* font_data = font.PrimaryFont(); + DCHECK(font_data); + String ellipsis_text = + font_data && font_data->GlyphForCharacter(kHorizontalEllipsisCharacter) + ? String(&kHorizontalEllipsisCharacter, 1) + : String(u"..."); + HarfBuzzShaper shaper(ellipsis_text.Characters16(), ellipsis_text.length()); + scoped_refptr<ShapeResult> ellipsis_shape_result = + shaper.Shape(&font, line_direction_); + LayoutUnit ellipsis_width = ellipsis_shape_result->SnappedWidth(); + + // Loop children from the logical last to the logical first to determine where + // to place the ellipsis. Children maybe truncated or moved as part of the + // process. + LayoutUnit ellipsis_inline_offset; + const NGPhysicalFragment* ellipsized_fragment = nullptr; + if (IsLtr(line_direction_)) { + NGLineBoxFragmentBuilder::Child* first_child = line_box->FirstInFlowChild(); + for (auto it = line_box->rbegin(); it != line_box->rend(); it++) { + auto& child = *it; + if (base::Optional<LayoutUnit> candidate = EllipsisOffset( + line_width, ellipsis_width, &child == first_child, &child)) { + ellipsis_inline_offset = candidate.value(); + ellipsized_fragment = child.PhysicalFragment(); + DCHECK(ellipsized_fragment); + break; + } + } + } else { + NGLineBoxFragmentBuilder::Child* first_child = line_box->LastInFlowChild(); + ellipsis_inline_offset = available_width_ - ellipsis_width; + for (auto& child : *line_box) { + if (base::Optional<LayoutUnit> candidate = EllipsisOffset( + line_width, ellipsis_width, &child == first_child, &child)) { + ellipsis_inline_offset = candidate.value(); + ellipsized_fragment = child.PhysicalFragment(); + DCHECK(ellipsized_fragment); + break; + } + } + } + + // Abort if ellipsis could not be placed. + if (!ellipsized_fragment) + return line_width; + + // Now the offset of the ellpisis is determined. Place the ellpisis into the + // line box. + NGTextFragmentBuilder builder(node_, line_style_->GetWritingMode()); + DCHECK(ellipsized_fragment->GetLayoutObject() && + ellipsized_fragment->GetLayoutObject()->IsInline()); + builder.SetText(ellipsized_fragment->GetLayoutObject(), ellipsis_text, + ellipsis_style, true /* is_ellipsis_style */, + std::move(ellipsis_shape_result)); + FontBaseline baseline_type = line_style_->GetFontBaseline(); + NGLineHeightMetrics ellipsis_metrics(font_data->GetFontMetrics(), + baseline_type); + line_box->AddChild( + builder.ToTextFragment(), + NGLogicalOffset{ellipsis_inline_offset, -ellipsis_metrics.ascent}, + ellipsis_width, 0); + return std::max(ellipsis_inline_offset + ellipsis_width, line_width); +} + +// Return the offset to place the ellipsis. +// +// This function may truncate or move the child so that the ellipsis can fit. +base::Optional<LayoutUnit> NGLineTruncator::EllipsisOffset( + LayoutUnit line_width, + LayoutUnit ellipsis_width, + bool is_first_child, + NGLineBoxFragmentBuilder::Child* child) { + // Leave out-of-flow children as is. + if (!child->HasInFlowFragment()) + return base::nullopt; + + // Can't place ellipsis if this child is completely outside of the box. + DCHECK_GE(line_width, child->offset.inline_offset + child->inline_size); + LayoutUnit child_inline_offset = + IsLtr(line_direction_) + ? child->offset.inline_offset + : line_width - (child->offset.inline_offset + child->inline_size); + LayoutUnit space_for_child = available_width_ - child_inline_offset; + if (space_for_child <= 0) + return base::nullopt; + + // If not all of this child can fit, try to truncate. + space_for_child -= ellipsis_width; + if (space_for_child < child->inline_size && + !TruncateChild(space_for_child, is_first_child, child)) { + // This child maybe partially visible. When it can't be truncated, move it + // out so that none of this child should be visible. + child->offset.inline_offset = line_width; + return base::nullopt; + } + + return IsLtr(line_direction_) + ? child->offset.inline_offset + child->inline_size + : child->offset.inline_offset - ellipsis_width; +} + +// Truncate the specified child. Returns true if truncated successfully, false +// otherwise. +// +// Note that this function may return true even if it can't fit the child when +// |is_first_child|, because the spec defines that the first character or atomic +// inline-level element on a line must be clipped rather than ellipsed. +// https://drafts.csswg.org/css-ui/#text-overflow +bool NGLineTruncator::TruncateChild(LayoutUnit space_for_child, + bool is_first_child, + NGLineBoxFragmentBuilder::Child* child) { + // If the space is not enough, try the next child. + if (space_for_child <= 0 && !is_first_child) + return false; + + // Only text fragments can be truncated. + if (!child->fragment) + return is_first_child; + auto& fragment = ToNGPhysicalTextFragment(*child->fragment); + const ShapeResult* shape_result = fragment.TextShapeResult(); + if (!shape_result) + return is_first_child; + + // Compute the offset to truncate. + unsigned new_length = shape_result->OffsetToFit( + IsLtr(line_direction_) ? space_for_child + : shape_result->Width() - space_for_child, + line_direction_); + if (!new_length || new_length == fragment.Length()) { + if (!is_first_child) + return false; + new_length = !new_length ? 1 : new_length - 1; + } + + // Truncate the text fragment. + child->fragment = line_direction_ == shape_result->Direction() + ? fragment.TrimText(fragment.StartOffset(), + fragment.StartOffset() + new_length) + : fragment.TrimText(fragment.StartOffset() + new_length, + fragment.EndOffset()); + LayoutUnit new_inline_size = line_style_->IsHorizontalWritingMode() + ? child->fragment->Size().width + : child->fragment->Size().height; + DCHECK_LE(new_inline_size, child->inline_size); + if (UNLIKELY(IsRtl(line_direction_))) + child->offset.inline_offset += child->inline_size - new_inline_size; + child->inline_size = new_inline_size; + return true; +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_truncator.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_truncator.h new file mode 100644 index 00000000000..adfcd22ad9c --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_truncator.h @@ -0,0 +1,51 @@ +// 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_LINE_TRUNCATOR_H_ +#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_LINE_TRUNCATOR_H_ + +#include "base/optional.h" +#include "third_party/blink/renderer/core/core_export.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment_builder.h" +#include "third_party/blink/renderer/platform/text/text_direction.h" + +namespace blink { + +class NGLineInfo; + +// A class to truncate lines and place ellipsis, invoked by the CSS +// 'text-overflow: ellipsis' property. +// https://drafts.csswg.org/css-ui/#overflow-ellipsis +class CORE_EXPORT NGLineTruncator final { + STACK_ALLOCATED(); + + public: + NGLineTruncator(NGInlineNode& node, const NGLineInfo& line_info); + + // Truncate |line_box| and place ellipsis. Returns the new inline-size of the + // |line_box|. + // + // |line_box| should be after bidi reorder, but before box fragments are + // created. + LayoutUnit TruncateLine(LayoutUnit line_width, + NGLineBoxFragmentBuilder::ChildList* line_box); + + private: + base::Optional<LayoutUnit> EllipsisOffset(LayoutUnit line_width, + LayoutUnit ellipsis_width, + bool is_first_child, + NGLineBoxFragmentBuilder::Child*); + bool TruncateChild(LayoutUnit space_for_this_child, + bool is_first_child, + NGLineBoxFragmentBuilder::Child* child); + + NGInlineNode& node_; + scoped_refptr<const ComputedStyle> line_style_; + LayoutUnit available_width_; + TextDirection line_direction_; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_LINE_TRUNCATOR_H_ diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_utils.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_utils.cc new file mode 100644 index 00000000000..d4c9f60a712 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_utils.cc @@ -0,0 +1,31 @@ +// 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_line_utils.h" + +#include "third_party/blink/renderer/core/editing/position_with_affinity.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.h" +#include "third_party/blink/renderer/core/paint/ng/ng_paint_fragment.h" + +namespace blink { + +const NGPaintFragment* NGContainingLineBoxOf( + const PositionWithAffinity& position) { + const NGCaretPosition caret_position = ComputeNGCaretPosition(position); + if (caret_position.IsNull()) + return nullptr; + return caret_position.fragment->ContainerLineBox(); +} + +bool InSameNGLineBox(const PositionWithAffinity& position1, + const PositionWithAffinity& position2) { + const NGPaintFragment* line_box1 = NGContainingLineBoxOf(position1); + if (!line_box1) + return false; + + const NGPaintFragment* line_box2 = NGContainingLineBoxOf(position2); + return line_box1 == line_box2; +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_utils.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_utils.h new file mode 100644 index 00000000000..e77e8dbde26 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_utils.h @@ -0,0 +1,25 @@ +// 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_LINE_UTILS_H_ +#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_LINE_UTILS_H_ + +#include "third_party/blink/renderer/core/editing/forward.h" + +namespace blink { + +class NGPaintFragment; + +// Returns the NG line box fragment containing the caret position of the given +// position. Returns false if the position is not in Layout NG, or does not +// have any caret position. +const NGPaintFragment* NGContainingLineBoxOf(const PositionWithAffinity&); + +// Returns true if the caret positions of the two positions are in the same NG +// line box. Returns false in all other cases. +bool InSameNGLineBox(const PositionWithAffinity&, const PositionWithAffinity&); + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_LINE_UTILS_H_ 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 ff4406cef52..46e7077f983 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 @@ -10,6 +10,7 @@ #include "third_party/blink/renderer/core/editing/position.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" namespace blink { @@ -247,13 +248,13 @@ NGMappingUnitRange NGOffsetMapping::GetMappingUnitsForDOMRange( return {result_begin, result_end}; } -Optional<unsigned> NGOffsetMapping::GetTextContentOffset( +base::Optional<unsigned> NGOffsetMapping::GetTextContentOffset( const Position& position) const { DCHECK(NGOffsetMapping::AcceptsPosition(position)) << position; if (IsNonAtomicInline(*position.AnchorNode())) { auto iter = ranges_.find(position.AnchorNode()); if (iter == ranges_.end()) - return WTF::nullopt; + return base::nullopt; DCHECK_NE(iter->value.first, iter->value.second) << position; if (position.IsBeforeAnchor()) return units_[iter->value.first].TextContentStart(); @@ -262,7 +263,7 @@ Optional<unsigned> NGOffsetMapping::GetTextContentOffset( const NGOffsetMappingUnit* unit = GetMappingUnitForPosition(position); if (!unit) - return WTF::nullopt; + return base::nullopt; return unit->ConvertDOMOffsetToTextContent(ToNodeOffsetPair(position).second); } @@ -339,13 +340,13 @@ bool NGOffsetMapping::IsAfterNonCollapsedContent( unit->GetType() != NGOffsetMappingUnitType::kCollapsed; } -Optional<UChar> NGOffsetMapping::GetCharacterBefore( +base::Optional<UChar> NGOffsetMapping::GetCharacterBefore( const Position& position) const { DCHECK(NGOffsetMapping::AcceptsPosition(position)); DCHECK(!IsNonAtomicInline(*position.AnchorNode())) << position; - Optional<unsigned> text_content_offset = GetTextContentOffset(position); + base::Optional<unsigned> text_content_offset = GetTextContentOffset(position); if (!text_content_offset || !*text_content_offset) - return WTF::nullopt; + return base::nullopt; return text_[*text_content_offset - 1]; } @@ -385,4 +386,15 @@ Position NGOffsetMapping::GetLastPosition(unsigned offset) const { return CreatePositionForOffsetMapping(node, dom_offset); } +bool NGOffsetMapping::HasBidiControlCharactersOnly(unsigned start, + unsigned end) const { + DCHECK_LE(start, end); + DCHECK_LE(end, text_.length()); + for (unsigned i = start; i < end; ++i) { + if (!Character::IsBidiControl(text_[i])) + return false; + } + return true; +} + } // namespace blink 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 f4ea52ba8f5..efb2769eb30 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 @@ -5,12 +5,13 @@ #ifndef NGOffsetMapping_h #define NGOffsetMapping_h +#include "base/optional.h" #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/platform/heap/handle.h" #include "third_party/blink/renderer/platform/wtf/allocator.h" #include "third_party/blink/renderer/platform/wtf/hash_map.h" -#include "third_party/blink/renderer/platform/wtf/optional.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" #include "third_party/blink/renderer/platform/wtf/vector.h" @@ -18,7 +19,6 @@ namespace blink { class LayoutBlockFlow; class LayoutObject; -class Node; enum class NGOffsetMappingUnitType { kIdentity, kCollapsed, kExpanded }; @@ -134,7 +134,7 @@ class CORE_EXPORT NGOffsetMapping { // Returns the text content offset corresponding to the given position. // Returns nullopt when the position is not laid out in this context. - Optional<unsigned> GetTextContentOffset(const Position&) const; + base::Optional<unsigned> GetTextContentOffset(const Position&) const; // Starting from the given position, searches for non-collapsed content in // the anchor node in forward/backward direction and returns the position @@ -151,7 +151,7 @@ class CORE_EXPORT NGOffsetMapping { // Maps the given position to a text content offset, and then returns the text // content character before the offset. Returns nullopt if it does not exist. - Optional<UChar> GetCharacterBefore(const Position&) const; + base::Optional<UChar> GetCharacterBefore(const Position&) const; // ------ Mapping APIs from text content to DOM ------ @@ -172,6 +172,12 @@ class CORE_EXPORT NGOffsetMapping { // TODO(xiaochengh): Add offset-to-DOM APIs skipping generated contents. + // ------ APIs inspecting the text content string ------ + + // Returns false if all characters in [start, end) of |text_| are bidi + // control charcters. Returns true otherwise. + bool HasBidiControlCharactersOnly(unsigned start, unsigned end) const; + private: // The NGOffsetMappingUnits of the inline formatting context in osrted order. UnitVector units_; diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping_builder.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping_builder.h index 8451f04475a..716d3ac760e 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping_builder.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping_builder.h @@ -5,9 +5,9 @@ #ifndef NGOffsetMappingBuilder_h #define NGOffsetMappingBuilder_h +#include "base/auto_reset.h" #include "third_party/blink/renderer/core/core_export.h" #include "third_party/blink/renderer/platform/wtf/allocator.h" -#include "third_party/blink/renderer/platform/wtf/auto_reset.h" #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" #include "third_party/blink/renderer/platform/wtf/vector.h" @@ -80,7 +80,7 @@ class CORE_EXPORT NGOffsetMappingBuilder { ~SourceNodeScope(); private: - AutoReset<const LayoutObject*> auto_reset_; + base::AutoReset<const LayoutObject*> auto_reset_; #if DCHECK_IS_ON() NGOffsetMappingBuilder* builder_ = nullptr; 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 39232f8ad19..1036a000106 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 @@ -40,7 +40,7 @@ class NGOffsetMappingTest : public NGLayoutTest { } bool IsOffsetMappingStored() const { - return layout_block_flow_->GetNGInlineNodeData()->offset_mapping_.get(); + return layout_block_flow_->GetNGInlineNodeData()->offset_mapping.get(); } const LayoutText* GetLayoutTextUnder(const char* parent_id) { @@ -53,7 +53,8 @@ class NGOffsetMappingTest : public NGLayoutTest { return GetOffsetMapping().GetMappingUnitForPosition(position); } - Optional<unsigned> GetTextContentOffset(const Position& position) const { + base::Optional<unsigned> GetTextContentOffset( + const Position& position) const { return GetOffsetMapping().GetTextContentOffset(position); } 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 10d79eb6c43..e341ddffdae 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.cc @@ -5,26 +5,61 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.h" #include "third_party/blink/renderer/core/style/computed_style.h" namespace blink { +namespace { + +static const NGPhysicalFragment* LastLogicalLeafExceptLinebreakInternal( + const NGPhysicalFragment& runner, + TextDirection direction) { + if (runner.IsText()) { + if (ToNGPhysicalTextFragment(runner).IsLineBreak()) + return nullptr; + return &runner; + } + if (!runner.IsContainer() || runner.IsBlockLayoutRoot()) + return &runner; + const auto& children = ToNGPhysicalContainerFragment(runner).Children(); + for (size_t i = 0; i < children.size(); i++) { + // TODO(xiaochengh): This isn't correct for mixed Bidi. Fix it. Besides, we + // should compute and store it during layout. + // We want a logical last child in a line. + const size_t index = + direction == TextDirection::kLtr ? (children.size() - 1 - i) : i; + const NGPhysicalFragment* child = children[index].get(); + DCHECK(child); + if (const NGPhysicalFragment* candidate = + LastLogicalLeafExceptLinebreakInternal(*child, direction)) + return candidate; + } + return nullptr; +} + +} // namespace + NGPhysicalLineBoxFragment::NGPhysicalLineBoxFragment( const ComputedStyle& style, + NGStyleVariant style_variant, NGPhysicalSize size, Vector<scoped_refptr<NGPhysicalFragment>>& children, const NGPhysicalOffsetRect& contents_visual_rect, + const NGPhysicalOffsetRect& scrollable_overflow, const NGLineHeightMetrics& metrics, TextDirection base_direction, scoped_refptr<NGBreakToken> break_token) : NGPhysicalContainerFragment(nullptr, style, + style_variant, size, kFragmentLineBox, 0, children, contents_visual_rect, std::move(break_token)), + scrollable_overflow_(scrollable_overflow), metrics_(metrics) { base_direction_ = static_cast<unsigned>(base_direction); } @@ -80,6 +115,13 @@ const NGPhysicalFragment* NGPhysicalLineBoxFragment::LastLogicalLeaf() const { return runner; } +const NGPhysicalFragment* +NGPhysicalLineBoxFragment::LastLogicalLeafIgnoringLineBreak() const { + if (Children().IsEmpty()) + return nullptr; + return LastLogicalLeafExceptLinebreakInternal(*this, this->BaseDirection()); +} + bool NGPhysicalLineBoxFragment::HasSoftWrapToNextLine() const { DCHECK(BreakToken()); DCHECK(BreakToken()->IsInlineType()); @@ -87,9 +129,4 @@ bool NGPhysicalLineBoxFragment::HasSoftWrapToNextLine() const { return !break_token.IsFinished() && !break_token.IsForcedBreak(); } -PositionWithAffinity NGPhysicalLineBoxFragment::PositionForPoint( - const NGPhysicalOffset& point) const { - return PositionForPointInInlineLevelBox(point); -} - } // 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 02989a0955e..b92b910d2b3 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 @@ -17,9 +17,11 @@ class CORE_EXPORT NGPhysicalLineBoxFragment final public: // This modifies the passed-in children vector. NGPhysicalLineBoxFragment(const ComputedStyle&, + NGStyleVariant style_variant, NGPhysicalSize size, Vector<scoped_refptr<NGPhysicalFragment>>& children, const NGPhysicalOffsetRect& contents_visual_rect, + const NGPhysicalOffsetRect& scrollable_overflow, const NGLineHeightMetrics&, TextDirection base_direction, scoped_refptr<NGBreakToken> break_token = nullptr); @@ -39,24 +41,31 @@ class CORE_EXPORT NGPhysicalLineBoxFragment final // VisualRect of itself including contents, in the local coordinate. NGPhysicalOffsetRect VisualRectWithContents() const; + // Scrollable overflow. including contents, in the local coordinate. + NGPhysicalOffsetRect ScrollableOverflow() const { + return scrollable_overflow_; + } + // Returns the first/last leaf fragment in the line in logical order. Returns // nullptr if the line box is empty. const NGPhysicalFragment* FirstLogicalLeaf() const; const NGPhysicalFragment* LastLogicalLeaf() const; + // Returns the last leaf fragment in the line in logical order except line + // break. Returns nullptr if such fragment doesn't exist. + const NGPhysicalFragment* LastLogicalLeafIgnoringLineBreak() const; // Whether the content soft-wraps to the next line. bool HasSoftWrapToNextLine() const; - PositionWithAffinity PositionForPoint(const NGPhysicalOffset&) const final; - scoped_refptr<NGPhysicalFragment> CloneWithoutOffset() const { Vector<scoped_refptr<NGPhysicalFragment>> children_copy(children_); return base::AdoptRef(new NGPhysicalLineBoxFragment( - Style(), size_, children_copy, contents_visual_rect_, metrics_, - BaseDirection(), break_token_)); + Style(), StyleVariant(), size_, children_copy, contents_visual_rect_, + scrollable_overflow_, metrics_, BaseDirection(), break_token_)); } private: + NGPhysicalOffsetRect scrollable_overflow_; NGLineHeightMetrics metrics_; }; diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment_test.cc index cd44fd8ea05..331fb87d70b 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment_test.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_line_box_fragment_test.cc @@ -59,6 +59,7 @@ TEST_F(NGPhysicalLineBoxFragmentTest, FirstLastLogicalLeafInSimpleText) { "</div>"); EXPECT_TEXT_FRAGMENT("foo", GetLineBox()->FirstLogicalLeaf()); EXPECT_TEXT_FRAGMENT("bar", GetLineBox()->LastLogicalLeaf()); + EXPECT_TEXT_FRAGMENT("bar", GetLineBox()->LastLogicalLeafIgnoringLineBreak()); } TEST_F(NGPhysicalLineBoxFragmentTest, FirstLastLogicalLeafInRtlText) { @@ -69,6 +70,7 @@ TEST_F(NGPhysicalLineBoxFragmentTest, FirstLastLogicalLeafInRtlText) { "</bdo>"); EXPECT_TEXT_FRAGMENT("foo", GetLineBox()->FirstLogicalLeaf()); EXPECT_TEXT_FRAGMENT("bar", GetLineBox()->LastLogicalLeaf()); + EXPECT_TEXT_FRAGMENT("bar", GetLineBox()->LastLogicalLeafIgnoringLineBreak()); } TEST_F(NGPhysicalLineBoxFragmentTest, @@ -81,6 +83,7 @@ TEST_F(NGPhysicalLineBoxFragmentTest, "</div>"); EXPECT_TEXT_FRAGMENT("f", GetLineBox()->FirstLogicalLeaf()); EXPECT_TEXT_FRAGMENT("r", GetLineBox()->LastLogicalLeaf()); + EXPECT_TEXT_FRAGMENT("r", GetLineBox()->LastLogicalLeafIgnoringLineBreak()); } TEST_F(NGPhysicalLineBoxFragmentTest, FirstLastLogicalLeafWithInlineBlock) { @@ -92,12 +95,26 @@ TEST_F(NGPhysicalLineBoxFragmentTest, FirstLastLogicalLeafWithInlineBlock) { "</div>"); EXPECT_BOX_FRAGMENT("foo", GetLineBox()->FirstLogicalLeaf()); EXPECT_BOX_FRAGMENT("baz", GetLineBox()->LastLogicalLeaf()); + EXPECT_BOX_FRAGMENT("baz", GetLineBox()->LastLogicalLeafIgnoringLineBreak()); } TEST_F(NGPhysicalLineBoxFragmentTest, FirstLastLogicalLeafWithImages) { SetBodyInnerHTML("<div id=root><img id=img1>foo<img id=img2></div>"); EXPECT_BOX_FRAGMENT("img1", GetLineBox()->FirstLogicalLeaf()); EXPECT_BOX_FRAGMENT("img2", GetLineBox()->LastLogicalLeaf()); + EXPECT_BOX_FRAGMENT("img2", GetLineBox()->LastLogicalLeafIgnoringLineBreak()); +} + +TEST_F(NGPhysicalLineBoxFragmentTest, LastLogicalLeafSoftWrap) { + SetBodyInnerHTML("<div id=root style='width: 2em'>foo bar</div>"); + EXPECT_TEXT_FRAGMENT("foo", GetLineBox()->LastLogicalLeaf()); + EXPECT_TEXT_FRAGMENT("foo", GetLineBox()->LastLogicalLeafIgnoringLineBreak()); +} + +TEST_F(NGPhysicalLineBoxFragmentTest, LastLogicalLeafHardWrap) { + SetBodyInnerHTML("<div id=root>foo<br>bar</div>"); + EXPECT_TEXT_FRAGMENT("\n", GetLineBox()->LastLogicalLeaf()); + EXPECT_TEXT_FRAGMENT("foo", GetLineBox()->LastLogicalLeafIgnoringLineBreak()); } } // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_physical_text_fragment.cc index 848ed377dd8..d75f591cc0f 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,16 +5,31 @@ #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/editing/position_with_affinity.h" #include "third_party/blink/renderer/core/layout/layout_text_fragment.h" -#include "third_party/blink/renderer/core/layout/ng/geometry/ng_logical_rect.h" +#include "third_party/blink/renderer/core/layout/line/line_orientation_utils.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_line_height_metrics.h" -#include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h" #include "third_party/blink/renderer/core/style/computed_style.h" namespace blink { +// Convert logical cooridnate to local physical coordinate. +NGPhysicalOffsetRect NGPhysicalTextFragment::ConvertToLocal( + const LayoutRect& logical_rect) const { + switch (LineOrientation()) { + case NGLineOrientation::kHorizontal: + return NGPhysicalOffsetRect(logical_rect); + case NGLineOrientation::kClockWiseVertical: + 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()}}; + } + NOTREACHED(); + return NGPhysicalOffsetRect(logical_rect); +} + // Compute the inline position from text offset, in logical coordinate relative // to this fragment. LayoutUnit NGPhysicalTextFragment::InlinePositionForOffset( @@ -75,8 +90,8 @@ NGPhysicalOffsetRect NGPhysicalTextFragment::LocalRect( } NGPhysicalOffsetRect NGPhysicalTextFragment::SelfVisualRect() const { - if (!shape_result_) - return {}; + if (UNLIKELY(!shape_result_)) + return LocalRect(); // Glyph bounds is in logical coordinate, origin at the alphabetic baseline. LayoutRect visual_rect = EnclosingLayoutRect(shape_result_->Bounds()); @@ -84,8 +99,7 @@ NGPhysicalOffsetRect NGPhysicalTextFragment::SelfVisualRect() const { // Make the origin at the logical top of this fragment. const ComputedStyle& style = Style(); const Font& font = style.GetFont(); - const SimpleFontData* font_data = font.PrimaryFont(); - if (font_data) { + if (const SimpleFontData* font_data = font.PrimaryFont()) { visual_rect.SetY(visual_rect.Y() + font_data->GetFontMetrics().FixedAscent( kAlphabeticBaseline)); } @@ -111,26 +125,38 @@ NGPhysicalOffsetRect NGPhysicalTextFragment::SelfVisualRect() const { if (ShadowList* text_shadow = style.TextShadow()) { LayoutRectOutsets text_shadow_logical_outsets = - LayoutRectOutsets(text_shadow->RectOutsetsIncludingOriginal()) - .LineOrientationOutsets(style.GetWritingMode()); + LineOrientationLayoutRectOutsets( + LayoutRectOutsets(text_shadow->RectOutsetsIncludingOriginal()), + style.GetWritingMode()); text_shadow_logical_outsets.ClampNegativeToZero(); visual_rect.Expand(text_shadow_logical_outsets); } visual_rect = LayoutRect(EnclosingIntRect(visual_rect)); - switch (LineOrientation()) { - case NGLineOrientation::kHorizontal: - return NGPhysicalOffsetRect(visual_rect); - case NGLineOrientation::kClockWiseVertical: - return {{size_.width - visual_rect.MaxY(), visual_rect.X()}, - {visual_rect.Height(), visual_rect.Width()}}; - case NGLineOrientation::kCounterClockWiseVertical: - return {{visual_rect.Y(), size_.height - visual_rect.MaxX()}, - {visual_rect.Height(), visual_rect.Width()}}; - } - NOTREACHED(); - return {}; + // Uniting the frame rect ensures that non-ink spaces such side bearings, or + // even space characters, are included in the visual rect for decorations. + NGPhysicalOffsetRect local_visual_rect = ConvertToLocal(visual_rect); + local_visual_rect.Unite(LocalRect()); + return local_visual_rect; +} + +scoped_refptr<NGPhysicalFragment> NGPhysicalTextFragment::TrimText( + unsigned new_start_offset, + unsigned new_end_offset) const { + DCHECK(shape_result_); + DCHECK_GE(new_start_offset, StartOffset()); + DCHECK_GT(new_end_offset, new_start_offset); + DCHECK_LE(new_end_offset, EndOffset()); + scoped_refptr<ShapeResult> new_shape_result = + shape_result_->SubRange(new_start_offset, new_end_offset); + LayoutUnit new_inline_size = new_shape_result->SnappedWidth(); + return base::AdoptRef(new NGPhysicalTextFragment( + layout_object_, Style(), static_cast<NGStyleVariant>(style_variant_), + TextType(), text_, new_start_offset, new_end_offset, + IsHorizontal() ? NGPhysicalSize{new_inline_size, size_.height} + : NGPhysicalSize{size_.width, new_inline_size}, + LineOrientation(), EndEffect(), std::move(new_shape_result))); } scoped_refptr<NGPhysicalFragment> NGPhysicalTextFragment::CloneWithoutOffset() @@ -164,15 +190,24 @@ unsigned NGPhysicalTextFragment::TextOffsetForPoint( StartOffset(); } -PositionWithAffinity NGPhysicalTextFragment::PositionForPoint( - const NGPhysicalOffset& point) const { - if (IsAnonymousText()) - return PositionWithAffinity(); - const unsigned text_offset = TextOffsetForPoint(point); - const Position position = - NGOffsetMapping::GetFor(GetLayoutObject())->GetFirstPosition(text_offset); - // TODO(xiaochengh): Adjust TextAffinity. - return PositionWithAffinity(position, TextAffinity::kDownstream); +UBiDiLevel NGPhysicalTextFragment::BidiLevel() const { + // TODO(xiaochengh): Make the implementation more efficient with, e.g., + // binary search and/or LayoutNGText::InlineItems(). + const auto& items = InlineItemsOfContainingBlock(); + const NGInlineItem* containing_item = std::find_if( + items.begin(), items.end(), [this](const NGInlineItem& item) { + return item.StartOffset() <= StartOffset() && + item.EndOffset() >= EndOffset(); + }); + DCHECK(containing_item); + DCHECK_NE(containing_item, items.end()); + return containing_item->BidiLevel(); +} + +TextDirection NGPhysicalTextFragment::ResolvedDirection() const { + if (TextShapeResult()) + return TextShapeResult()->Direction(); + return NGPhysicalFragment::ResolvedDirection(); } } // namespace blink 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 eea1dddd920..f6921d10546 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 @@ -8,7 +8,6 @@ #include "third_party/blink/renderer/core/core_export.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_text_end_effect.h" #include "third_party/blink/renderer/core/layout/ng/ng_physical_fragment.h" -#include "third_party/blink/renderer/platform/fonts/font_baseline.h" #include "third_party/blink/renderer/platform/fonts/ng_text_fragment_paint_info.h" #include "third_party/blink/renderer/platform/fonts/shaping/shape_result.h" #include "third_party/blink/renderer/platform/wtf/text/string_view.h" @@ -16,8 +15,6 @@ namespace blink { -class ShapeResult; - struct NGPhysicalOffsetRect; enum class AdjustMidCluster; @@ -104,9 +101,6 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { bool IsHorizontal() const { return LineOrientation() == NGLineOrientation::kHorizontal; } - FontBaseline BaselineType() const { - return IsHorizontal() ? kAlphabeticBaseline : kIdeographicBaseline; - } // Compute the inline position from text offset, in logical coordinate // relative to this fragment. @@ -116,6 +110,7 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { // Start and end offsets must be between StartOffset() and EndOffset(). NGPhysicalOffsetRect 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. @@ -125,6 +120,11 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { return static_cast<NGTextEndEffect>(end_effect_); } + // Create a new fragment that has part of the text of this fragment. + // All other properties are the same as this fragment. + scoped_refptr<NGPhysicalFragment> TrimText(unsigned start_offset, + unsigned end_offset) const; + scoped_refptr<NGPhysicalFragment> CloneWithoutOffset() const; NGTextFragmentPaintInfo PaintInfo() const { @@ -139,13 +139,16 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { // Returns the text offset in the fragment placed closest to the given point. unsigned TextOffsetForPoint(const NGPhysicalOffset&) const; - PositionWithAffinity PositionForPoint(const NGPhysicalOffset&) const override; + UBiDiLevel BidiLevel() const override; + TextDirection ResolvedDirection() const override; private: LayoutUnit InlinePositionForOffset(unsigned offset, LayoutUnit (*round)(float), AdjustMidCluster) const; + NGPhysicalOffsetRect ConvertToLocal(const LayoutRect&) 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. const String text_; 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 8852bb2da74..9ee319a8d3c 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 @@ -37,13 +37,14 @@ NGTextFragmentBuilder::NGTextFragmentBuilder(NGInlineNode node, void NGTextFragmentBuilder::SetItem( NGPhysicalTextFragment::NGTextType text_type, + const NGInlineItemsData& items_data, NGInlineItemResult* item_result, LayoutUnit line_height) { DCHECK(item_result); DCHECK(item_result->item->Style()); text_type_ = text_type; - text_ = inline_node_.Text(); + text_ = items_data.text_content; item_index_ = item_result->item_index; start_offset_ = item_result->start_offset; end_offset_ = item_result->end_offset; @@ -71,11 +72,8 @@ void NGTextFragmentBuilder::SetText( end_offset_ = shape_result->EndIndexForResult(); SetStyle(style, is_ellipsis_style ? NGStyleVariant::kEllipsis : NGStyleVariant::kStandard); - FontBaseline baseline_type = style->IsHorizontalWritingMode() - ? kAlphabeticBaseline - : kIdeographicBaseline; size_ = {shape_result->SnappedWidth(), - NGLineHeightMetrics(*style, baseline_type).LineHeight()}; + NGLineHeightMetrics(*style).LineHeight()}; shape_result_ = std::move(shape_result); layout_object_ = layout_object; end_effect_ = NGTextEndEffect::kNone; 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 b53ca079a0b..68efb30de15 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 @@ -26,6 +26,7 @@ class CORE_EXPORT NGTextFragmentBuilder final : public NGBaseFragmentBuilder { // NOTE: Takes ownership of the shape result within the item result. void SetItem(NGPhysicalTextFragment::NGTextType, + const NGInlineItemsData&, NGInlineItemResult*, LayoutUnit line_height); void SetText(LayoutObject*, |