diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-01-20 13:40:20 +0100 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-01-22 12:41:23 +0000 |
commit | 7961cea6d1041e3e454dae6a1da660b453efd238 (patch) | |
tree | c0eeb4a9ff9ba32986289c1653d9608e53ccb444 /chromium/third_party/blink/renderer/core/layout/ng/inline | |
parent | b7034d0803538058e5c9d904ef03cf5eab34f6ef (diff) | |
download | qtwebengine-chromium-7961cea6d1041e3e454dae6a1da660b453efd238.tar.gz |
BASELINE: Update Chromium to 78.0.3904.130
Change-Id: If185e0c0061b3437531c97c9c8c78f239352a68b
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
Diffstat (limited to 'chromium/third_party/blink/renderer/core/layout/ng/inline')
35 files changed, 2483 insertions, 353 deletions
diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/layout_ng_text_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/layout_ng_text_test.cc new file mode 100644 index 00000000000..da4b412bbff --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/layout_ng_text_test.cc @@ -0,0 +1,270 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/core/layout/ng/inline/layout_ng_text.h" + +#include <sstream> +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.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/testing/page_test_base.h" +#include "third_party/blink/renderer/platform/runtime_enabled_features.h" +#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h" + +namespace blink { + +class LayoutNGTextTest : public PageTestBase { + protected: + std::string GetItemsAsString(const LayoutText& layout_text) { + if (layout_text.NeedsCollectInlines()) + return "LayoutText has NeedsCollectInlines"; + if (!layout_text.HasValidInlineItems()) + return "No valid inline items in LayoutText"; + const LayoutBlockFlow& block_flow = *layout_text.ContainingNGBlockFlow(); + if (block_flow.NeedsCollectInlines()) + return "LayoutBlockFlow has NeedsCollectInlines"; + const NGInlineNodeData& data = *block_flow.GetNGInlineNodeData(); + std::ostringstream stream; + for (const NGInlineItem& item : data.items) { + if (item.Type() != NGInlineItem::kText) + continue; + if (item.GetLayoutObject() == layout_text) + stream << "*"; + stream << "{'" + << data.text_content.Substring(item.StartOffset(), item.Length()) + .Utf8() + << "'"; + if (item.TextShapeResult()) { + stream << ", ShapeResult=" << item.TextShapeResult()->StartIndex() + << "+" << item.TextShapeResult()->NumCharacters(); + } + stream << "}" << std::endl; + } + return stream.str(); + } +}; + +TEST_F(LayoutNGTextTest, SetTextWithOffsetAppendCollapseWhiteSpace) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<p id=target>abc </p>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()); + text.appendData("XYZ"); + + EXPECT_EQ("*{'abc XYZ', ShapeResult=0+7}\n", + GetItemsAsString(*text.GetLayoutObject())); +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetAppend) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZ<b>def</b></pre>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling()); + text.appendData("xyz"); + + EXPECT_EQ( + "{'abc', ShapeResult=0+3}\n" + "*{'XYZxyz', ShapeResult=3+6}\n" + "{'def', ShapeResult=9+3}\n", + GetItemsAsString(*text.GetLayoutObject())); +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetDelete) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<pre id=target><a>abc</a>xXYZyz<b>def</b></pre>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling()); + text.deleteData(1, 3, ASSERT_NO_EXCEPTION); + + EXPECT_EQ( + "{'abc', ShapeResult=0+3}\n" + "*{'xyz', ShapeResult=3+3}\n" + "{'def', ShapeResult=6+3}\n", + GetItemsAsString(*text.GetLayoutObject())); +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetDeleteCollapseWhiteSpace) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<p id=target>ab XY cd</p>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()); + text.deleteData(4, 2, ASSERT_NO_EXCEPTION); // remove "XY" + + EXPECT_EQ("*{'ab cd', ShapeResult=0+5}\n", + GetItemsAsString(*text.GetLayoutObject())); +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetDeleteCollapseWhiteSpaceEnd) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<p id=target>a bc</p>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()); + text.deleteData(2, 2, ASSERT_NO_EXCEPTION); // remove "bc" + + EXPECT_EQ("*{'a', ShapeResult=0+1}\n", + GetItemsAsString(*text.GetLayoutObject())); +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetDeleteRTL) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<p id=target dir=rtl>0 234</p>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()); + text.deleteData(2, 2, ASSERT_NO_EXCEPTION); // remove "23" + + EXPECT_EQ("*{'0 4', ShapeResult=0+3}\n", + GetItemsAsString(*text.GetLayoutObject())); +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetInsert) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZ<b>def</b></pre>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling()); + text.insertData(1, "xyz", ASSERT_NO_EXCEPTION); + + EXPECT_EQ( + "{'abc', ShapeResult=0+3}\n" + "*{'XxyzYZ', ShapeResult=3+6}\n" + "{'def', ShapeResult=9+3}\n", + GetItemsAsString(*text.GetLayoutObject())); +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetInsertAfterSpace) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<p id=target>ab cd</p>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()); + text.insertData(3, " XYZ ", ASSERT_NO_EXCEPTION); + + EXPECT_EQ("*{'ab XYZ cd', ShapeResult=0+9}\n", + GetItemsAsString(*text.GetLayoutObject())); +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetInserBeforetSpace) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<p id=target>ab cd</p>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()); + text.insertData(2, " XYZ ", ASSERT_NO_EXCEPTION); + + EXPECT_EQ("*{'ab XYZ cd', ShapeResult=0+9}\n", + GetItemsAsString(*text.GetLayoutObject())); +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetNoRelocation) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZ<b>def</b></pre>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling()); + // Note: |CharacterData::setData()| is implementation of Node::setNodeValue() + // for |CharacterData|. + text.setData("xyz"); + + EXPECT_EQ("LayoutText has NeedsCollectInlines", + GetItemsAsString(*text.GetLayoutObject())) + << "There are no optimization for setData()"; +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetPrepend) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZ<b>def</b></pre>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling()); + text.insertData(1, "xyz", ASSERT_NO_EXCEPTION); + + EXPECT_EQ( + "{'abc', ShapeResult=0+3}\n" + "*{'XxyzYZ', ShapeResult=3+6}\n" + "{'def', ShapeResult=9+3}\n", + GetItemsAsString(*text.GetLayoutObject())); +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetReplace) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZW<b>def</b></pre>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling()); + text.replaceData(1, 2, "yz", ASSERT_NO_EXCEPTION); + + EXPECT_EQ( + "{'abc', ShapeResult=0+3}\n" + "*{'XyzW', ShapeResult=3+4}\n" + "{'def', ShapeResult=7+3}\n", + GetItemsAsString(*text.GetLayoutObject())); +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetReplaceCollapseWhiteSpace) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<p id=target>ab XY cd</p>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()); + text.replaceData(4, 2, " ", ASSERT_NO_EXCEPTION); // replace "XY" to " " + + EXPECT_EQ("*{'ab cd', ShapeResult=0+5}\n", + GetItemsAsString(*text.GetLayoutObject())); +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetReplaceToExtend) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZW<b>def</b></pre>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling()); + text.replaceData(1, 2, "xyz", ASSERT_NO_EXCEPTION); + + EXPECT_EQ( + "{'abc', ShapeResult=0+3}\n" + "*{'XxyzW', ShapeResult=3+5}\n" + "{'def', ShapeResult=8+3}\n", + GetItemsAsString(*text.GetLayoutObject())); +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetReplaceToShrink) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZW<b>def</b></pre>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling()); + text.replaceData(1, 2, "y", ASSERT_NO_EXCEPTION); + + EXPECT_EQ( + "{'abc', ShapeResult=0+3}\n" + "*{'XyW', ShapeResult=3+3}\n" + "{'def', ShapeResult=6+3}\n", + GetItemsAsString(*text.GetLayoutObject())); +} + +TEST_F(LayoutNGTextTest, SetTextWithOffsetToEmpty) { + if (!RuntimeEnabledFeatures::LayoutNGEnabled()) + return; + + SetBodyInnerHTML(u"<pre id=target><a>abc</a>XYZ<b>def</b></pre>"); + Text& text = To<Text>(*GetElementById("target")->firstChild()->nextSibling()); + // Note: |CharacterData::setData()| is implementation of Node::setNodeValue() + // for |CharacterData|. + // Note: |setData()| detaches layout object from |Text| node since + // |Text::TextLayoutObjectIsNeeded()| returns false for empty text. + text.setData(""); + UpdateAllLifecyclePhasesForTest(); + + EXPECT_EQ(nullptr, text.GetLayoutObject()); +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.cc index 4c1c7ee28da..cf430a648e4 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.cc @@ -256,4 +256,11 @@ scoped_refptr<AbstractInlineTextBox> NGAbstractInlineTextBox::PreviousOnLine() return nullptr; } +bool NGAbstractInlineTextBox::IsLineBreak() const { + if (!fragment_) + return false; + DCHECK(!NeedsLayout()); + return PhysicalTextFragment().IsLineBreak(); +} + } // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.h index fb2368ed230..551e5642114 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_abstract_inline_text_box.h @@ -50,6 +50,7 @@ class CORE_EXPORT NGAbstractInlineTextBox final : public AbstractInlineTextBox { bool IsLast() const final; scoped_refptr<AbstractInlineTextBox> NextOnLine() const final; scoped_refptr<AbstractInlineTextBox> PreviousOnLine() const final; + bool IsLineBreak() const final; const NGPaintFragment* fragment_; diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_baseline.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_baseline.h index e527615ce8b..4f4a1978af6 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_baseline.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_baseline.h @@ -5,6 +5,7 @@ #ifndef NGBaseline_h #define NGBaseline_h +#include "base/optional.h" #include "third_party/blink/renderer/core/core_export.h" #include "third_party/blink/renderer/platform/fonts/font_baseline.h" #include "third_party/blink/renderer/platform/geometry/layout_unit.h" diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item.cc new file mode 100644 index 00000000000..35aa96a8c03 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item.cc @@ -0,0 +1,155 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item.h" + +#include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items_builder.h" +#include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h" + +namespace blink { + +NGFragmentItem::NGFragmentItem(const NGPhysicalTextFragment& text) + : layout_object_(text.GetLayoutObject()), + text_({text.TextShapeResult(), text.StartOffset(), text.EndOffset()}), + rect_({PhysicalOffset(), text.Size()}), + type_(kText), + style_variant_(static_cast<unsigned>(text.StyleVariant())), + is_hidden_for_paint_(false) { + DCHECK_LE(text_.start_offset, text_.end_offset); +#if DCHECK_IS_ON() + if (text_.shape_result) { + DCHECK_EQ(text_.shape_result->StartIndex(), text_.start_offset); + DCHECK_EQ(text_.shape_result->EndIndex(), text_.end_offset); + } +#endif +} + +NGFragmentItem::NGFragmentItem(const NGPhysicalLineBoxFragment& line, + wtf_size_t item_count) + : layout_object_(nullptr), + line_({line.Metrics(), To<NGInlineBreakToken>(line.BreakToken()), + item_count}), + rect_({PhysicalOffset(), line.Size()}), + type_(kLine), + style_variant_(static_cast<unsigned>(line.StyleVariant())), + is_hidden_for_paint_(false) {} + +NGFragmentItem::NGFragmentItem(const NGPhysicalBoxFragment& box, + wtf_size_t item_count) + : layout_object_(box.GetLayoutObject()), + box_({&box, item_count}), + rect_({PhysicalOffset(), box.Size()}), + type_(kBox), + style_variant_(static_cast<unsigned>(box.StyleVariant())), + is_hidden_for_paint_(false) {} + +NGFragmentItem::~NGFragmentItem() { + switch (Type()) { + case kText: + text_.~TextItem(); + break; + case kGeneratedText: + generated_text_.~GeneratedTextItem(); + break; + case kLine: + line_.~LineItem(); + break; + case kBox: + box_.~BoxItem(); + break; + } +} + +PhysicalRect NGFragmentItem::SelfInkOverflow() const { + // TODO(kojii): Implement. + return LocalRect(); +} + +StringView NGFragmentItem::Text(const NGFragmentItems& items) const { + if (Type() == kText) { + DCHECK_LE(text_.start_offset, text_.end_offset); + return StringView(items.Text(UsesFirstLineStyle()), text_.start_offset, + text_.end_offset - text_.start_offset); + } + NOTREACHED(); + return StringView(); +} + +NGTextFragmentPaintInfo NGFragmentItem::TextPaintInfo( + const NGFragmentItems& items) const { + if (Type() == kText) { + return {items.Text(UsesFirstLineStyle()), text_.start_offset, + text_.end_offset, text_.shape_result.get()}; + } + NOTREACHED(); + return {}; +} + +String NGFragmentItem::DebugName() const { + return "NGFragmentItem"; +} + +IntRect NGFragmentItem::VisualRect() const { + // TODO(kojii): Need to reconsider the storage of |VisualRect|, to integrate + // better with |FragmentData| and to avoid dependency to |LayoutObject|. + return GetLayoutObject()->VisualRectForInlineBox(); +} + +PhysicalRect NGFragmentItem::LocalVisualRectFor( + const LayoutObject& layout_object) { + DCHECK(RuntimeEnabledFeatures::LayoutNGFragmentItemEnabled()); + DCHECK(layout_object.IsInLayoutNGInlineFormattingContext()); + + PhysicalRect visual_rect; + for (const NGFragmentItem& item : ItemsFor(layout_object)) { + if (UNLIKELY(item.IsHiddenForPaint())) + continue; + PhysicalRect child_visual_rect = item.SelfInkOverflow(); + child_visual_rect.offset += item.Offset(); + visual_rect.Unite(child_visual_rect); + } + return visual_rect; +} + +NGFragmentItem::ItemsForLayoutObject NGFragmentItem::ItemsFor( + const LayoutObject& layout_object) { + DCHECK(layout_object.IsInLayoutNGInlineFormattingContext()); + DCHECK(layout_object.IsText() || layout_object.IsLayoutInline() || + (layout_object.IsBox() && layout_object.IsInline())); + + // TODO(kojii): This is a hot function needed by paint and several other + // operations. Make this fast, by not iterating. + if (const LayoutBlockFlow* block_flow = + layout_object.RootInlineFormattingContext()) { + if (const NGPhysicalBoxFragment* fragment = block_flow->CurrentFragment()) { + if (const NGFragmentItems* items = fragment->Items()) { + for (unsigned i = 0; i < items->Items().size(); ++i) { + const NGFragmentItem* item = items->Items()[i].get(); + if (item->GetLayoutObject() == &layout_object) + return ItemsForLayoutObject(items->Items(), i, item); + } + } + } + } + + return ItemsForLayoutObject(); +} + +NGFragmentItem::ItemsForLayoutObject::Iterator& +NGFragmentItem::ItemsForLayoutObject::Iterator::operator++() { + // TODO(kojii): This is a hot function needed by paint and several other + // operations. Make this fast, by not iterating. + if (!current_) + return *this; + const LayoutObject* current_layout_object = current_->GetLayoutObject(); + while (++index_ < items_->size()) { + current_ = (*items_)[index_].get(); + if (current_->GetLayoutObject() == current_layout_object) + return *this; + } + current_ = nullptr; + return *this; +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item.h new file mode 100644 index 00000000000..cf7cfc2803a --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item.h @@ -0,0 +1,231 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_FRAGMENT_ITEM_H_ +#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_FRAGMENT_ITEM_H_ + +#include "third_party/blink/renderer/core/core_export.h" +#include "third_party/blink/renderer/core/layout/geometry/logical_offset.h" +#include "third_party/blink/renderer/core/layout/layout_object.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment_builder.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_line_height_metrics.h" +#include "third_party/blink/renderer/platform/graphics/paint/display_item_client.h" + +namespace blink { + +class NGFragmentItems; +class NGInlineBreakToken; +struct NGTextFragmentPaintInfo; + +// This class represents a text run or a box in an inline formatting context. +// +// This class consumes less memory than a full fragment, and can be stored in a +// flat list (NGFragmentItems) for easier and faster traversal. +class CORE_EXPORT NGFragmentItem : public DisplayItemClient { + public: + // Represents regular text that exists in the DOM. + struct TextItem { + scoped_refptr<const ShapeResultView> shape_result; + // TODO(kojii): |start_offset| and |end_offset| should match to the offset + // in |shape_result|. Consider if we should remove them, or if keeping them + // is easier. + const unsigned start_offset; + const unsigned end_offset; + }; + // Represents text generated by the layout engine, e.g., hyphen or ellipsis. + struct GeneratedTextItem { + scoped_refptr<const ShapeResultView> shape_result; + String text; + }; + // A start marker of a line box. + struct LineItem { + NGLineHeightMetrics metrics; + scoped_refptr<const NGInlineBreakToken> inline_break_token; + wtf_size_t children_count; + }; + // Represents a box fragment appeared in a line. This includes inline boxes + // (e.g., <span>text</span>) and atomic inlines. + struct BoxItem { + // If this item is an inline box, its children are stored as following + // items. |children_count_| has the number of such items. + // + // If this item is a root of another IFC/BFC, children are stored normally, + // as children of |box_fragment|. + scoped_refptr<const NGPhysicalBoxFragment> box_fragment; + wtf_size_t children_count; + }; + + enum ItemType { kText, kGeneratedText, kLine, kBox }; + + // TODO(kojii): Should be able to create without once creating fragments. + NGFragmentItem(const NGPhysicalTextFragment& text); + NGFragmentItem(const NGPhysicalBoxFragment& box, wtf_size_t item_count); + NGFragmentItem(const NGPhysicalLineBoxFragment& line, wtf_size_t item_count); + + ~NGFragmentItem() final; + + ItemType Type() const { return static_cast<ItemType>(type_); } + + bool IsHiddenForPaint() const { return is_hidden_for_paint_; } + + NGStyleVariant StyleVariant() const { + return static_cast<NGStyleVariant>(style_variant_); + } + bool UsesFirstLineStyle() const { + return StyleVariant() == NGStyleVariant::kFirstLine; + } + // Returns the style for this fragment. + // + // For a line box, this returns the style of the containing block. This mostly + // represents the style for the line box, except 1) |style.Direction()| maybe + // incorrect, use |BaseDirection()| instead, and 2) margin/border/padding, + // background etc. do not apply to the line box. + const ComputedStyle& Style() const { + return layout_object_->EffectiveStyle(StyleVariant()); + } + const LayoutObject* GetLayoutObject() const { return layout_object_; } + + const PhysicalRect& Rect() const { return rect_; } + const PhysicalOffset& Offset() const { return rect_.offset; } + const PhysicalSize& Size() const { return rect_.size; } + void SetOffset(const PhysicalOffset& offset) { rect_.offset = offset; } + + PhysicalRect LocalRect() const { return {PhysicalOffset(), Size()}; } + PhysicalRect SelfInkOverflow() const; + + // Count of following items that are descendants of this item in the box tree, + // including this item. 1 means this is a box (box or line box) without + // children. 0 if this item type cannot have children. + wtf_size_t ChildrenCount() const { + if (Type() == kBox) + return box_.children_count; + if (Type() == kLine) + return line_.children_count; + return 0; + } + + // Returns |NGPhysicalBoxFragment| if one is associated with this item. + const NGPhysicalBoxFragment* BoxFragment() const { + if (Type() == kBox) + return box_.box_fragment.get(); + return nullptr; + } + + unsigned TextLength() const { + DCHECK_EQ(Type(), kText); + return text_.end_offset - text_.start_offset; + } + StringView Text(const NGFragmentItems& items) const; + NGTextFragmentPaintInfo TextPaintInfo(const NGFragmentItems& items) const; + + // DisplayItemClient overrides + String DebugName() const override; + IntRect VisualRect() const override; + + // Find |NGFragmentItem|s that are associated with a |LayoutObject|. + class CORE_EXPORT ItemsForLayoutObject { + STACK_ALLOCATED(); + + public: + ItemsForLayoutObject() = default; + ItemsForLayoutObject(const Vector<std::unique_ptr<NGFragmentItem>>& items, + unsigned first_index, + const NGFragmentItem* first_item) + : items_(&items), first_item_(first_item), first_index_(first_index) {} + + bool IsEmpty() const { return !items_; } + + class CORE_EXPORT Iterator { + public: + Iterator(const Vector<std::unique_ptr<NGFragmentItem>>* items, + unsigned index, + const NGFragmentItem* item) + : current_(item), items_(items), index_(index) {} + const NGFragmentItem& operator*() const { return *current_; } + const NGFragmentItem& operator->() const { return *current_; } + Iterator& operator++(); + bool operator==(const Iterator& other) const { + return current_ == other.current_; + } + bool operator!=(const Iterator& other) const { + return current_ != other.current_; + } + + private: + const NGFragmentItem* current_; + const Vector<std::unique_ptr<NGFragmentItem>>* items_; + unsigned index_; + }; + using iterator = Iterator; + iterator begin() const { + return Iterator(items_, first_index_, first_item_); + } + iterator end() const { return Iterator(nullptr, 0, nullptr); } + + private: + const Vector<std::unique_ptr<NGFragmentItem>>* items_; + const NGFragmentItem* first_item_; + unsigned first_index_; + }; + static ItemsForLayoutObject ItemsFor(const LayoutObject& layout_object); + static PhysicalRect LocalVisualRectFor(const LayoutObject& layout_object); + + // Painters can use const methods only, except for these explicitly declared + // methods. + class MutableForPainting { + STACK_ALLOCATED(); + + public: + // TODO(kojii): Add painter functions. + + private: + friend class NGFragmentItem; + MutableForPainting(const NGFragmentItem& item) {} + }; + MutableForPainting GetMutableForPainting() const { + return MutableForPainting(*this); + } + + private: + const LayoutObject* layout_object_; + + // TODO(kojii): We can make them sub-classes if we need to make the vector of + // pointers. Sub-classing from DisplayItemClient prohibits copying and that we + // cannot create a vector of this class. + union { + TextItem text_; + GeneratedTextItem generated_text_; + LineItem line_; + BoxItem box_; + }; + + PhysicalRect rect_; + + struct NGInkOverflowModel { + USING_FAST_MALLOC(NGInkOverflowModel); + + public: + NGInkOverflowModel(const PhysicalRect& self_ink_overflow, + const PhysicalRect& contents_ink_overflow); + + PhysicalRect self_ink_overflow; + // TODO(kojii): Some types (e.g., kText) never have |contents_ink_overflow|. + // Can/should we optimize the memory usage for those cases? + PhysicalRect contents_ink_overflow; + }; + mutable std::unique_ptr<NGInkOverflowModel> ink_overflow_; + // TOOD(kojii): mutable because this is lazily computed, but it may not be + // needed if we use |MutableForPainting|. TBD. + + // Item index delta to the next item for the same |LayoutObject|. + // wtf_size_t delta_to_next_for_same_layout_object_ = 0; + + unsigned type_ : 2; // ItemType + unsigned style_variant_ : 2; // NGStyleVariant + unsigned is_hidden_for_paint_ : 1; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_FRAGMENT_ITEM_H_ diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item_test.cc new file mode 100644 index 00000000000..bc573b0d0d1 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item_test.cc @@ -0,0 +1,123 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item.h" + +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/renderer/core/layout/layout_block_flow.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items.h" +#include "third_party/blink/renderer/core/layout/ng/ng_layout_test.h" +#include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h" + +using testing::ElementsAre; + +namespace blink { + +class NGFragmentItemTest : public NGLayoutTest, + ScopedLayoutNGFragmentItemForTest { + public: + NGFragmentItemTest() : ScopedLayoutNGFragmentItemForTest(true) {} + + Vector<const NGFragmentItem*> ItemsForAsVector( + const LayoutObject& layout_object) { + const auto items = NGFragmentItem::ItemsFor(layout_object); + Vector<const NGFragmentItem*> list; + for (const NGFragmentItem& item : items) { + EXPECT_EQ(item.GetLayoutObject(), &layout_object); + list.push_back(&item); + } + return list; + } +}; + +TEST_F(NGFragmentItemTest, BasicText) { + LoadAhem(); + SetBodyInnerHTML(R"HTML( + <style> + html, body { + margin: 0; + font-family: Ahem; + font-size: 10px; + line-height: 1; + } + div { + width: 10ch; + } + </style> + <div id="container"> + 1234567 98765 + </div> + )HTML"); + + LayoutBlockFlow* container = + To<LayoutBlockFlow>(GetLayoutObjectByElementId("container")); + LayoutText* layout_text = ToLayoutText(container->FirstChild()); + const NGPhysicalBoxFragment* box = container->CurrentFragment(); + EXPECT_NE(box, nullptr); + const NGFragmentItems* items = box->Items(); + EXPECT_NE(items, nullptr); + EXPECT_EQ(items->Items().size(), 4u); + + // The text node wraps, produces two fragments. + Vector<const NGFragmentItem*> items_for_text = ItemsForAsVector(*layout_text); + EXPECT_EQ(items_for_text.size(), 2u); + + const NGFragmentItem& text1 = *items_for_text[0]; + EXPECT_EQ(text1.Type(), NGFragmentItem::kText); + EXPECT_EQ(text1.GetLayoutObject(), layout_text); + EXPECT_EQ(text1.Offset(), PhysicalOffset()); + + const NGFragmentItem& text2 = *items_for_text[1]; + EXPECT_EQ(text2.Type(), NGFragmentItem::kText); + EXPECT_EQ(text2.GetLayoutObject(), layout_text); + EXPECT_EQ(text2.Offset(), PhysicalOffset(0, 10)); + + EXPECT_EQ(IntRect(0, 0, 70, 20), + layout_text->FragmentsVisualRectBoundingBox()); +} + +TEST_F(NGFragmentItemTest, BasicInlineBox) { + LoadAhem(); + SetBodyInnerHTML(R"HTML( + <style> + html, body { + margin: 0; + font-family: Ahem; + font-size: 10px; + line-height: 1; + } + #container { + width: 10ch; + } + #span1, #span2 { + background: gray; + } + </style> + <div id="container"> + 000 + <span id="span1">1234 5678</span> + 999 + <span id="span2">12345678</span> + </div> + )HTML"); + + // "span1" wraps, produces two fragments. + const LayoutObject* span1 = GetLayoutObjectByElementId("span1"); + ASSERT_NE(span1, nullptr); + Vector<const NGFragmentItem*> items_for_span1 = ItemsForAsVector(*span1); + EXPECT_EQ(items_for_span1.size(), 2u); + + EXPECT_EQ(IntRect(0, 0, 80, 20), span1->FragmentsVisualRectBoundingBox()); + + // "span2" doesn't wrap, produces only one fragment. + const LayoutObject* span2 = GetLayoutObjectByElementId("span2"); + ASSERT_NE(span2, nullptr); + Vector<const NGFragmentItem*> items_for_span2 = ItemsForAsVector(*span2); + EXPECT_EQ(items_for_span2.size(), 1u); + + EXPECT_EQ(IntRect(0, 20, 80, 10), span2->FragmentsVisualRectBoundingBox()); +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items.cc new file mode 100644 index 00000000000..87aba81920f --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items.cc @@ -0,0 +1,16 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items.h" + +#include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items_builder.h" + +namespace blink { + +NGFragmentItems::NGFragmentItems(NGFragmentItemsBuilder* builder) + : items_(std::move(builder->items_)), + text_content_(std::move(builder->text_content_)), + first_line_text_content_(std::move(builder->first_line_text_content_)) {} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items.h new file mode 100644 index 00000000000..fe62f758e81 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items.h @@ -0,0 +1,40 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_FRAGMENT_ITEMS_H_ +#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_FRAGMENT_ITEMS_H_ + +#include "third_party/blink/renderer/core/core_export.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item.h" + +namespace blink { + +class NGFragmentItemsBuilder; + +// Represents the inside of an inline formatting context. +// +// During the layout phase, descendants of the inline formatting context is +// transformed to a flat list of |NGFragmentItem| and stored in this class. +class CORE_EXPORT NGFragmentItems { + public: + NGFragmentItems(NGFragmentItemsBuilder* builder); + + const Vector<std::unique_ptr<NGFragmentItem>>& Items() const { + return items_; + } + + const String& Text(bool first_line) const { + return UNLIKELY(first_line) ? first_line_text_content_ : text_content_; + } + + private: + // TODO(kojii): inline capacity TBD. + Vector<std::unique_ptr<NGFragmentItem>> items_; + String text_content_; + String first_line_text_content_; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_FRAGMENT_ITEMS_H_ diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items_builder.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items_builder.cc new file mode 100644 index 00000000000..31a90c76b39 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items_builder.cc @@ -0,0 +1,151 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items_builder.h" + +#include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items.h" +#include "third_party/blink/renderer/core/layout/ng/ng_box_fragment_builder.h" +#include "third_party/blink/renderer/core/layout/ng/ng_physical_box_fragment.h" + +namespace blink { + +void NGFragmentItemsBuilder::SetTextContent(const NGInlineNode& node) { + const NGInlineItemsData& items_data = node.ItemsData(false); + text_content_ = items_data.text_content; + const NGInlineItemsData& first_line = node.ItemsData(true); + if (&items_data != &first_line) + first_line_text_content_ = first_line.text_content; +} + +void NGFragmentItemsBuilder::SetCurrentLine( + const NGPhysicalLineBoxFragment& line, + ChildList&& children) { +#if DCHECK_IS_ON() + current_line_fragment_ = &line; +#endif + current_line_ = std::move(children); +} + +void NGFragmentItemsBuilder::AddLine(const NGPhysicalLineBoxFragment& line, + const LogicalOffset& offset) { + DCHECK_EQ(items_.size(), offsets_.size()); +#if DCHECK_IS_ON() + DCHECK(!is_converted_to_physical_); + DCHECK_EQ(current_line_fragment_, &line); +#endif + + // Reserve the capacity for (children + line box item). + wtf_size_t size_before = items_.size(); + wtf_size_t capacity = size_before + current_line_.size() + 1; + items_.ReserveCapacity(capacity); + offsets_.ReserveCapacity(capacity); + + // Add an empty item so that the start of the line can be set later. + wtf_size_t line_start_index = items_.size(); + items_.Grow(line_start_index + 1); + offsets_.Grow(line_start_index + 1); + + AddItems(current_line_.begin(), current_line_.end()); + + // All children are added. Create an item for the start of the line. + wtf_size_t item_count = items_.size() - line_start_index; + items_[line_start_index] = std::make_unique<NGFragmentItem>(line, item_count); + // TODO(kojii): We probably need an end marker too for the reverse-order + // traversals. + + for (unsigned i = size_before; i < offsets_.size(); ++i) + offsets_[i] += offset; + + current_line_.clear(); +#if DCHECK_IS_ON() + current_line_fragment_ = nullptr; +#endif +} + +void NGFragmentItemsBuilder::AddItems(Child* child_begin, Child* child_end) { + DCHECK_EQ(items_.size(), offsets_.size()); + + for (Child* child_iter = child_begin; child_iter != child_end;) { + Child& child = *child_iter; + if (const NGPhysicalTextFragment* text = child.fragment.get()) { + items_.push_back(std::make_unique<NGFragmentItem>(*text)); + offsets_.push_back(child.offset); + ++child_iter; + continue; + } + + if (child.layout_result) { + // Create an item if this box has no inline children. + const NGPhysicalBoxFragment& box = + To<NGPhysicalBoxFragment>(child.layout_result->PhysicalFragment()); + if (child.children_count <= 1) { + items_.push_back(std::make_unique<NGFragmentItem>(box, 1)); + offsets_.push_back(child.offset); + ++child_iter; + continue; + } + + // Children of inline boxes are flattened and added to |items_|, with the + // count of descendant items to preserve the tree structure. + // + // Add an empty item so that the start of the box can be set later. + wtf_size_t box_start_index = items_.size(); + items_.Grow(box_start_index + 1); + offsets_.push_back(child.offset); + + // Add all children, including their desendants, skipping this item. + CHECK_GE(child.children_count, 1u); // 0 will loop infinitely. + Child* end_child_iter = child_iter + child.children_count; + CHECK_LE(end_child_iter - child_begin, child_end - child_begin); + AddItems(child_iter + 1, end_child_iter); + child_iter = end_child_iter; + + // All children are added. Compute how many items are actually added. The + // number of items added maybe different from |child.children_count|. + wtf_size_t item_count = items_.size() - box_start_index; + + // Create an item for the start of the box. + items_[box_start_index] = + std::make_unique<NGFragmentItem>(box, item_count); + continue; + } + + // OOF children should have been added to their parent box fragments. + // TODO(kojii): Consider handling them in NGFragmentItem too. + DCHECK(!child.out_of_flow_positioned_box); + ++child_iter; + } +} + +// Convert internal logical offsets to physical. Items are kept with logical +// offset until outer box size is determined. +void NGFragmentItemsBuilder::ConvertToPhysical(WritingMode writing_mode, + TextDirection direction, + const PhysicalSize& outer_size) { + CHECK_EQ(items_.size(), offsets_.size()); +#if DCHECK_IS_ON() + DCHECK(!is_converted_to_physical_); +#endif + + const LogicalOffset* offset_iter = offsets_.begin(); + for (auto& item : items_) { + item->SetOffset(offset_iter->ConvertToPhysical(writing_mode, direction, + outer_size, item->Size())); + ++offset_iter; + } + +#if DCHECK_IS_ON() + is_converted_to_physical_ = true; +#endif +} + +void NGFragmentItemsBuilder::ToFragmentItems(WritingMode writing_mode, + TextDirection direction, + const PhysicalSize& outer_size, + void* data) { + ConvertToPhysical(writing_mode, direction, outer_size); + new (data) NGFragmentItems(this); +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items_builder.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items_builder.h new file mode 100644 index 00000000000..fcd9a1d5dce --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items_builder.h @@ -0,0 +1,84 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_FRAGMENT_ITEMS_BUILDER_H_ +#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_FRAGMENT_ITEMS_BUILDER_H_ + +#include "third_party/blink/renderer/core/core_export.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_item.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_line_box_fragment_builder.h" + +namespace blink { + +class NGBoxFragmentBuilder; +class NGFragmentItem; +class NGFragmentItems; +class NGInlineNode; + +// This class builds |NGFragmentItems|. +// +// Once |NGFragmentItems| is built, it is immutable. +class CORE_EXPORT NGFragmentItemsBuilder { + STACK_ALLOCATED(); + + public: + NGFragmentItemsBuilder(NGBoxFragmentBuilder* box_builder) {} + + const String& TextContent(bool first_line) const { + return UNLIKELY(first_line && first_line_text_content_) + ? first_line_text_content_ + : text_content_; + } + void SetTextContent(const NGInlineNode& node); + + // The caller should create a |ChildList| for a complete line and add to this + // builder. + // + // Adding a line is a two-pass operation, because |NGInlineLayoutAlgorithm| + // creates and positions children within a line box, but its parent algorithm + // positions the line box. |SetCurrentLine| sets the children, and the next + // |AddLine| adds them. + // + // TODO(kojii): Moving |ChildList| is not cheap because it has inline + // capacity. Reconsider the ownership. + using Child = NGLineBoxFragmentBuilder::Child; + using ChildList = NGLineBoxFragmentBuilder::ChildList; + void SetCurrentLine(const NGPhysicalLineBoxFragment& line, + ChildList&& children); + void AddLine(const NGPhysicalLineBoxFragment& line, + const LogicalOffset& offset); + + // Build a |NGFragmentItems|. The builder cannot build twice because data set + // to this builder may be cleared. + void ToFragmentItems(WritingMode writing_mode, + TextDirection direction, + const PhysicalSize& outer_size, + void* data); + + private: + void AddItems(Child* child_begin, Child* child_end); + + void ConvertToPhysical(WritingMode writing_mode, + TextDirection direction, + const PhysicalSize& outer_size); + + Vector<std::unique_ptr<NGFragmentItem>> items_; + Vector<LogicalOffset> offsets_; + String text_content_; + String first_line_text_content_; + + // Keeps children of a line until the offset is determined. See |AddLine|. + ChildList current_line_; + +#if DCHECK_IS_ON() + const NGPhysicalLineBoxFragment* current_line_fragment_ = nullptr; + bool is_converted_to_physical_ = false; +#endif + + friend class NGFragmentItems; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_FRAGMENT_ITEMS_BUILDER_H_ diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.cc index 0bdaf394987..52a55b926ac 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 @@ -404,8 +404,6 @@ unsigned NGInlineLayoutStateStack::UpdateBoxDataFragmentRange( // Find the first line box item that should create a box fragment. for (; index < line_box->size(); index++) { NGLineBoxFragmentBuilder::Child* start = &(*line_box)[index]; - if (start->IsPlaceholder()) - continue; const unsigned box_data_index = start->box_data_index; if (!box_data_index) continue; @@ -423,8 +421,6 @@ unsigned NGInlineLayoutStateStack::UpdateBoxDataFragmentRange( const unsigned start_index = index; for (index++; index < line_box->size(); index++) { NGLineBoxFragmentBuilder::Child* end = &(*line_box)[index]; - if (end->IsPlaceholder()) - continue; // If we found another box that maybe included in this box, update it // first. Updating will change |end->box_data_index| so that we can @@ -572,18 +568,24 @@ void NGInlineLayoutStateStack::CreateBoxFragments( unsigned start = box_data.fragment_start; unsigned end = box_data.fragment_end; DCHECK_GT(end, start); - NGLineBoxFragmentBuilder::Child& start_child = (*line_box)[start]; + NGLineBoxFragmentBuilder::Child* child = &(*line_box)[start]; scoped_refptr<const NGLayoutResult> box_fragment = box_data.CreateBoxFragment(line_box); - if (!start_child.HasFragment()) { - start_child.layout_result = std::move(box_fragment); - start_child.offset = box_data.offset; + if (!child->HasFragment()) { + child->layout_result = std::move(box_fragment); + child->offset = box_data.offset; + child->children_count = end - start; } 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. + // TODO(kojii): With |NGFragmentItem|, all cases hit this code. Consider + // creating an empty item beforehand to avoid inserting. line_box->InsertChild(start, std::move(box_fragment), box_data.offset, LayoutUnit(), 0); + ChildInserted(start + 1); + child = &(*line_box)[start]; + child->children_count = end - start + 1; } } @@ -618,13 +620,7 @@ NGInlineLayoutStateStack::BoxData::CreateBoxFragment( for (unsigned i = fragment_start; i < fragment_end; i++) { NGLineBoxFragmentBuilder::Child& child = (*line_box)[i]; - if (child.layout_result) { - box.AddChild(child.layout_result->PhysicalFragment(), - child.offset - offset); - child.layout_result.reset(); - } else if (child.fragment) { - box.AddChild(std::move(child.fragment), child.offset - offset); - } else if (child.out_of_flow_positioned_box) { + if (child.out_of_flow_positioned_box) { DCHECK(item->GetLayoutObject()->IsLayoutInline()); NGBlockNode oof_box(ToLayoutBox(child.out_of_flow_positioned_box)); @@ -636,6 +632,22 @@ NGInlineLayoutStateStack::BoxData::CreateBoxFragment( box.AddOutOfFlowChildCandidate(oof_box, static_offset, child.container_direction); child.out_of_flow_positioned_box = nullptr; + continue; + } + + if (RuntimeEnabledFeatures::LayoutNGFragmentItemEnabled()) { + // |NGFragmentItems| has a flat list of all descendants, except OOF + // objects. Still creates |NGPhysicalBoxFragment|, but don't add children + // to it and keep them in the flat list. + continue; + } + + if (child.layout_result) { + box.AddChild(child.layout_result->PhysicalFragment(), + child.offset - offset); + child.layout_result.reset(); + } else if (child.fragment) { + box.AddChild(std::move(child.fragment), child.offset - offset); } } @@ -657,7 +669,7 @@ NGInlineLayoutStateStack::ApplyBaselineShift( // |pending_descendants|. LayoutUnit baseline_shift; if (!box->pending_descendants.IsEmpty()) { - NGLineHeightMetrics max = MetricsForTopAndBottomAlign(*box, *line_box); + bool has_top_or_bottom = false; for (NGPendingPositions& child : box->pending_descendants) { // In quirks mode, metrics is empty if no content. if (child.metrics.IsEmpty()) @@ -666,9 +678,6 @@ NGInlineLayoutStateStack::ApplyBaselineShift( case EVerticalAlign::kTextTop: baseline_shift = child.metrics.ascent + box->TextTop(baseline_type); break; - case EVerticalAlign::kTop: - baseline_shift = child.metrics.ascent - max.ascent; - break; case EVerticalAlign::kTextBottom: if (const SimpleFontData* font_data = box->style->GetFont().PrimaryFont()) { @@ -678,10 +687,11 @@ NGInlineLayoutStateStack::ApplyBaselineShift( break; } NOTREACHED(); - FALLTHROUGH; - case EVerticalAlign::kBottom: - baseline_shift = max.descent - child.metrics.descent; break; + case EVerticalAlign::kTop: + case EVerticalAlign::kBottom: + has_top_or_bottom = true; + continue; default: NOTREACHED(); continue; @@ -691,6 +701,32 @@ NGInlineLayoutStateStack::ApplyBaselineShift( line_box->MoveInBlockDirection(baseline_shift, child.fragment_start, child.fragment_end); } + // `top` and `bottom` need to be applied after all other values are applied, + // because they align to the maximum metrics, but the maximum metrics may + // depend on other pending descendants for this box. + if (has_top_or_bottom) { + NGLineHeightMetrics max = MetricsForTopAndBottomAlign(*box, *line_box); + for (NGPendingPositions& child : box->pending_descendants) { + switch (child.vertical_align) { + case EVerticalAlign::kTop: + baseline_shift = child.metrics.ascent - max.ascent; + break; + case EVerticalAlign::kBottom: + baseline_shift = max.descent - child.metrics.descent; + break; + case EVerticalAlign::kTextTop: + case EVerticalAlign::kTextBottom: + continue; + default: + NOTREACHED(); + continue; + } + child.metrics.Move(baseline_shift); + box->metrics.Unite(child.metrics); + line_box->MoveInBlockDirection(baseline_shift, child.fragment_start, + child.fragment_end); + } + } box->pending_descendants.clear(); } @@ -781,14 +817,26 @@ NGLineHeightMetrics NGInlineLayoutStateStack::MetricsForTopAndBottomAlign( // BoxData contains inline boxes to be created later. Take them into account. for (const BoxData& box_data : box_data_list_) { + // Except when the box has `vertical-align: top` or `bottom`. + DCHECK(box_data.item->Style()); + const ComputedStyle& style = *box_data.item->Style(); + EVerticalAlign vertical_align = style.VerticalAlign(); + if (vertical_align == EVerticalAlign::kTop || + vertical_align == EVerticalAlign::kBottom) + continue; + // |block_offset| is the top position when the baseline is at 0. LayoutUnit box_ascent = -line_box[box_data.fragment_end].offset.block_offset; - LayoutUnit box_descent = box_data.size.block_size - box_ascent; + NGLineHeightMetrics box_metrics(box_ascent, + box_data.size.block_size - box_ascent); // The top/bottom of inline boxes should not include their paddings. - box_ascent -= box_data.padding.line_over; - box_descent -= box_data.padding.line_under; - metrics.Unite(NGLineHeightMetrics(box_ascent, box_descent)); + box_metrics.ascent -= box_data.padding.line_over; + box_metrics.descent -= box_data.padding.line_under; + // Include the line-height property. The inline box has the height of the + // font metrics without the line-height included. + box_metrics.AddLeading(style.ComputedLineHeightAsFixed()); + metrics.Unite(box_metrics); } // In quirks mode, metrics is empty if no content. diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h index ba6599469b6..f5c93cd9d2b 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h @@ -69,15 +69,6 @@ class CORE_EXPORT NGInlineBreakToken final : public NGBreakToken { return flags_ & kIsForcedBreak; } - // When a previously laid out line box didn't fit in the current - // fragmentainer, and we have to lay it out again in the next fragmentainer, - // we need to skip floats associated with that line. The parent block layout - // algorithm will take care of any floats that broke and need to be resumed in - // the next fragmentainer. Dealing with them as part of line layout as well - // would result in duplicate fragments for the floats. - void SetIgnoreFloats() { ignore_floats_ = true; } - bool IgnoreFloats() const { return ignore_floats_; } - #if DCHECK_IS_ON() String ToString() const override; #endif diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_child_layout_context.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_child_layout_context.cc index b1da84d71d0..888119d970d 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_child_layout_context.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_child_layout_context.cc @@ -10,7 +10,7 @@ namespace { struct SameSizeAsNGInlineChildLayoutContext { base::Optional<NGInlineLayoutStateStack> box_states_; - void* pointer; + void* pointers[2]; unsigned number; }; diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_child_layout_context.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_child_layout_context.h index 059776134d3..360714b2902 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_child_layout_context.h +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_child_layout_context.h @@ -5,6 +5,7 @@ #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_INLINE_CHILD_LAYOUT_CONTEXT_H_ #define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_INLINE_CHILD_LAYOUT_CONTEXT_H_ +#include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items_builder.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_box_state.h" namespace blink { @@ -21,6 +22,11 @@ class NGInlineChildLayoutContext { STACK_ALLOCATED(); public: + NGFragmentItemsBuilder* ItemsBuilder() { return items_builder_; } + void SetItemsBuilder(NGFragmentItemsBuilder* builder) { + items_builder_ = builder; + } + // Returns the NGInlineLayoutStateStack in this context. bool HasBoxStates() const { return box_states_.has_value(); } NGInlineLayoutStateStack* BoxStates() { return &*box_states_; } @@ -40,6 +46,10 @@ class NGInlineChildLayoutContext { } private: + // TODO(kojii): Probably better to own |NGInlineChildLayoutContext|. While we + // transit, allocating separately is easier. + NGFragmentItemsBuilder* items_builder_ = nullptr; + base::Optional<NGInlineLayoutStateStack> box_states_; // The items and its index this context is set up for. diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.cc new file mode 100644 index 00000000000..74053c0f0ab --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.cc @@ -0,0 +1,181 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.h" + +#include "third_party/blink/renderer/core/layout/layout_block_flow.h" +#include "third_party/blink/renderer/core/layout/ng/inline/ng_fragment_items.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" + +namespace blink { + +void NGInlineCursor::MoveToItem(const ItemsSpan::iterator& iter) { + DCHECK((items_.data() || !items_.size()) && !root_paint_fragment_); + DCHECK(iter == items_.end() || + (&*iter >= items_.data() && iter < items_.end())); + item_iter_ = iter; + current_item_ = item_iter_ != items_.end() ? item_iter_->get() : nullptr; +} + +void NGInlineCursor::SetRoot(ItemsSpan items) { + DCHECK(items.data() || !items.size()); + DCHECK_EQ(root_paint_fragment_, nullptr); + DCHECK_EQ(current_paint_fragment_, nullptr); + items_ = items; + MoveToItem(items_.begin()); +} + +void NGInlineCursor::SetRoot(const NGFragmentItems& items) { + SetRoot(items.Items()); +} + +void NGInlineCursor::SetRoot(const NGPaintFragment& root_paint_fragment) { + DCHECK(&root_paint_fragment); + root_paint_fragment_ = &root_paint_fragment; + current_paint_fragment_ = root_paint_fragment.FirstChild(); +} + +NGInlineCursor::NGInlineCursor(const LayoutBlockFlow& block_flow) { + DCHECK(&block_flow); + + if (const NGPhysicalBoxFragment* fragment = block_flow.CurrentFragment()) { + if (const NGFragmentItems* items = fragment->Items()) { + SetRoot(*items); + return; + } + } + + if (const NGPaintFragment* paint_fragment = block_flow.PaintFragment()) { + SetRoot(*paint_fragment); + return; + } + + NOTREACHED(); +} + +NGInlineCursor::NGInlineCursor(const NGFragmentItems& items) { + SetRoot(items); +} + +NGInlineCursor::NGInlineCursor(const NGPaintFragment& root_paint_fragment) { + SetRoot(root_paint_fragment); +} + +bool NGInlineCursor::IsLineBox() const { + if (current_paint_fragment_) + return current_paint_fragment_->PhysicalFragment().IsLineBox(); + if (current_item_) + return current_item_->Type() == NGFragmentItem::kLine; + NOTREACHED(); + return false; +} + +const NGPhysicalBoxFragment* NGInlineCursor::CurrentBoxFragment() const { + if (current_paint_fragment_) { + return DynamicTo<NGPhysicalBoxFragment>( + ¤t_paint_fragment_->PhysicalFragment()); + } + if (current_item_) + return current_item_->BoxFragment(); + NOTREACHED(); + return nullptr; +} + +const LayoutObject* NGInlineCursor::CurrentLayoutObject() const { + if (current_paint_fragment_) + return current_paint_fragment_->GetLayoutObject(); + if (current_item_) + return current_item_->GetLayoutObject(); + NOTREACHED(); + return nullptr; +} + +const PhysicalOffset NGInlineCursor::CurrentOffset() const { + if (current_paint_fragment_) + return current_paint_fragment_->InlineOffsetToContainerBox(); + if (current_item_) + return current_item_->Offset(); + NOTREACHED(); + return PhysicalOffset(); +} + +void NGInlineCursor::MoveToNext() { + if (root_paint_fragment_) { + MoveToNextPaintFragment(); + return; + } + MoveToNextItem(); +} + +void NGInlineCursor::MoveToNextSkippingChildren() { + if (root_paint_fragment_) { + MoveToNextPaintFragmentSkippingChildren(); + return; + } + MoveToNextItemSkippingChildren(); +} + +void NGInlineCursor::MoveToNextItem() { + DCHECK((items_.data() || !items_.size()) && !root_paint_fragment_); + if (current_item_) { + DCHECK(item_iter_ != items_.end()); + ++item_iter_; + current_item_ = item_iter_ != items_.end() ? item_iter_->get() : nullptr; + } +} + +void NGInlineCursor::MoveToNextItemSkippingChildren() { + DCHECK((items_.data() || !items_.size()) && !root_paint_fragment_); + if (UNLIKELY(!current_item_)) + return; + // If the current item has |ChildrenCount|, add it to move to the next + // sibling, skipping all children and their descendants. + if (wtf_size_t children_count = current_item_->ChildrenCount()) { + MoveToItem(item_iter_ + children_count); + return; + } + return MoveToNextItem(); +} + +void NGInlineCursor::MoveToParentPaintFragment() { + DCHECK(root_paint_fragment_ && current_paint_fragment_); + const NGPaintFragment* parent = current_paint_fragment_->Parent(); + if (parent && parent != root_paint_fragment_) { + current_paint_fragment_ = parent; + return; + } + current_paint_fragment_ = nullptr; +} + +void NGInlineCursor::MoveToNextPaintFragment() { + DCHECK(root_paint_fragment_ && current_paint_fragment_); + if (const NGPaintFragment* child = current_paint_fragment_->FirstChild()) { + current_paint_fragment_ = child; + return; + } + MoveToNextPaintFragmentSkippingChildren(); +} + +void NGInlineCursor::MoveToNextSibilingPaintFragment() { + DCHECK(root_paint_fragment_ && current_paint_fragment_); + if (const NGPaintFragment* next = current_paint_fragment_->NextSibling()) { + current_paint_fragment_ = next; + return; + } + current_paint_fragment_ = nullptr; +} + +void NGInlineCursor::MoveToNextPaintFragmentSkippingChildren() { + DCHECK(root_paint_fragment_ && current_paint_fragment_); + while (!IsAtEnd()) { + if (const NGPaintFragment* next = current_paint_fragment_->NextSibling()) { + current_paint_fragment_ = next; + return; + } + MoveToParentPaintFragment(); + } +} + +} // namespace blink diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.h b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.h new file mode 100644 index 00000000000..6468d9d5a0a --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.h @@ -0,0 +1,98 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_INLINE_CURSOR_H_ +#define THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_INLINE_CURSOR_H_ + +#include "base/containers/span.h" +#include "third_party/blink/renderer/core/core_export.h" +#include "third_party/blink/renderer/platform/wtf/allocator/allocator.h" + +namespace blink { + +class LayoutObject; +class LayoutBlockFlow; +class NGFragmentItem; +class NGFragmentItems; +class NGPaintFragment; +class NGPhysicalBoxFragment; +struct PhysicalOffset; + +// This class traverses fragments in an inline formatting context. +// +// When constructed, the initial position is empty. Call |MoveToNext()| to move +// to the first fragment. +// +// TODO(kojii): |NGPaintFragment| should be gone when |NGPaintFragment| is +// deprecated and all its uses are removed. +class CORE_EXPORT NGInlineCursor { + STACK_ALLOCATED(); + + public: + NGInlineCursor(const LayoutBlockFlow& block_flow); + NGInlineCursor(const NGFragmentItems& items); + NGInlineCursor(const NGPaintFragment& root_paint_fragment); + + // + // Functions to query the current position. + // + + bool IsAtEnd() const { return !current_item_ && !current_paint_fragment_; } + explicit operator bool() const { return !IsAtEnd(); } + + // True if the current position is a line box. + bool IsLineBox() const; + + // |Current*| functions return an object for the current position. + const NGFragmentItem* CurrentItem() const { return current_item_; } + const NGPaintFragment* CurrentPaintFragment() const { + return current_paint_fragment_; + } + const NGPhysicalBoxFragment* CurrentBoxFragment() const; + const LayoutObject* CurrentLayoutObject() const; + + // The offset relative to the root of the inline formatting context. + const PhysicalOffset CurrentOffset() const; + + // + // Functions to move the current position. + // + + // Move the current position to the next fragment in pre-order DFS. Returns + // |true| if the move was successful. + void MoveToNext(); + + // Same as |MoveToNext| except that this skips children even if they exist. + void MoveToNextSkippingChildren(); + + // TODO(kojii): Add more variations as needed, NextSibling, + // NextSkippingChildren, Previous, etc. + + private: + using ItemsSpan = base::span<const std::unique_ptr<NGFragmentItem>>; + + void SetRoot(const NGFragmentItems& items); + void SetRoot(ItemsSpan items); + void SetRoot(const NGPaintFragment& root_paint_fragment); + + void MoveToItem(const ItemsSpan::iterator& iter); + void MoveToNextItem(); + void MoveToNextItemSkippingChildren(); + + void MoveToParentPaintFragment(); + void MoveToNextPaintFragment(); + void MoveToNextSibilingPaintFragment(); + void MoveToNextPaintFragmentSkippingChildren(); + + ItemsSpan items_; + ItemsSpan::iterator item_iter_; + const NGFragmentItem* current_item_ = nullptr; + + const NGPaintFragment* root_paint_fragment_ = nullptr; + const NGPaintFragment* current_paint_fragment_ = nullptr; +}; + +} // namespace blink + +#endif // THIRD_PARTY_BLINK_RENDERER_CORE_LAYOUT_NG_INLINE_NG_INLINE_CURSOR_H_ diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor_test.cc new file mode 100644 index 00000000000..dc23eb46ed4 --- /dev/null +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor_test.cc @@ -0,0 +1,128 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_cursor.h" + +#include "testing/gmock/include/gmock/gmock.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/blink/renderer/core/layout/layout_text.h" +#include "third_party/blink/renderer/core/layout/ng/ng_layout_test.h" + +namespace blink { + +using ::testing::ElementsAre; + +class NGInlineCursorTest : public NGLayoutTest, + private ScopedLayoutNGFragmentItemForTest, + public testing::WithParamInterface<bool> { + public: + NGInlineCursorTest() : ScopedLayoutNGFragmentItemForTest(GetParam()) {} + + protected: + Vector<String> ToDebugStringList(NGInlineCursor* cursor) { + Vector<String> list; + for (; *cursor; cursor->MoveToNext()) + list.push_back(ToDebugString(*cursor)); + return list; + } + + String ToDebugString(const NGInlineCursor& cursor) { + if (const LayoutObject* layout_object = cursor.CurrentLayoutObject()) { + if (const LayoutText* text = ToLayoutTextOrNull(layout_object)) + return text->GetText().StripWhiteSpace(); + + if (const Element* element = + DynamicTo<Element>(layout_object->GetNode())) { + if (const AtomicString& id = element->GetIdAttribute()) + return "#" + id; + } + + return layout_object->DebugName(); + } + + if (cursor.IsLineBox()) + return "#linebox"; + return "#null"; + } +}; + +INSTANTIATE_TEST_SUITE_P(NGInlineCursorTest, + NGInlineCursorTest, + testing::Bool()); + +TEST_P(NGInlineCursorTest, Next) { + SetBodyInnerHTML(R"HTML( + <style> + span { background: gray; } + </style> + <div id=root> + text1 + <span id="span1"> + text2 + <span id="span2"> + text3 + </span> + text4 + </span> + text5 + </div> + )HTML"); + + LayoutBlockFlow* block_flow = + To<LayoutBlockFlow>(GetLayoutObjectByElementId("root")); + NGInlineCursor cursor(*block_flow); + Vector<String> list = ToDebugStringList(&cursor); + EXPECT_THAT(list, ElementsAre("#linebox", "text1", "#span1", "text2", + "#span2", "text3", "text4", "text5")); +} + +TEST_P(NGInlineCursorTest, NextSkippingChildren) { + SetBodyInnerHTML(R"HTML( + <style> + span { background: gray; } + </style> + <div id=root> + text1 + <span id="span1"> + text2 + <span id="span2"> + text3 + </span> + text4 + </span> + text5 + </div> + )HTML"); + + LayoutBlockFlow* block_flow = + To<LayoutBlockFlow>(GetLayoutObjectByElementId("root")); + NGInlineCursor cursor(*block_flow); + for (unsigned i = 0; i < 3; ++i) + cursor.MoveToNext(); + EXPECT_EQ("text2", ToDebugString(cursor)); + Vector<String> list; + while (true) { + cursor.MoveToNextSkippingChildren(); + if (!cursor) + break; + list.push_back(ToDebugString(cursor)); + } + EXPECT_THAT(list, ElementsAre("#span2", "text4", "text5")); +} + +TEST_P(NGInlineCursorTest, EmptyOutOfFlow) { + SetBodyInnerHTML(R"HTML( + <div id=root> + <span style="position: absolute"></span> + </div> + )HTML"); + + LayoutBlockFlow* block_flow = + To<LayoutBlockFlow>(GetLayoutObjectByElementId("root")); + NGInlineCursor cursor(*block_flow); + Vector<String> list = ToDebugStringList(&cursor); + EXPECT_THAT(list, ElementsAre()); +} + +} // 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 81be1dd387d..0ae66308706 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 @@ -124,6 +124,10 @@ class CORE_EXPORT NGInlineItem { LayoutObject* GetLayoutObject() const { return layout_object_; } + bool IsImage() const { + return GetLayoutObject() && GetLayoutObject()->IsLayoutImage(); + } + void SetOffset(unsigned start, unsigned end) { DCHECK_GE(end, start); start_offset_ = start; @@ -250,6 +254,7 @@ class CORE_EXPORT NGInlineItem { unsigned is_symbol_marker_ : 1; unsigned is_generated_for_line_break_ : 1; friend class NGInlineNode; + friend class NGInlineNodeDataEditor; }; inline void NGInlineItem::AssertOffset(unsigned offset) const { 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 e73dbce4bd4..89f7f3474a7 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 @@ -1186,10 +1186,34 @@ void NGInlineItemsBuilderTemplate<OffsetMappingBuilder>::ExitInline( LayoutObject* node) { DCHECK(node); - AppendOpaque(NGInlineItem::kCloseTag, node); + if (NeedsBoxInfo()) { + BoxInfo* current_box = &boxes_.back(); + if (!current_box->should_create_box_fragment) { + // Set ShouldCreateBoxFragment if this inline box is empty so that we can + // compute its position/size correctly. Check this by looking for any + // non-empty items after the last |kOpenTag|. + const unsigned open_item_index = current_box->item_index; + DCHECK_GE(items_->size(), open_item_index + 1); + DCHECK_EQ((*items_)[open_item_index].Type(), NGInlineItem::kOpenTag); + for (unsigned i = items_->size() - 1;; --i) { + NGInlineItem& item = (*items_)[i]; + if (i == open_item_index) { + DCHECK_EQ(i, current_box->item_index); + // TODO(kojii): <area> element fails to hit-test when we don't cull. + if (!IsHTMLAreaElement(item.GetLayoutObject()->GetNode())) + item.SetShouldCreateBoxFragment(); + break; + } + DCHECK_GT(i, current_box->item_index); + if (!item.IsEmptyItem()) + break; + } + } - if (NeedsBoxInfo()) boxes_.pop_back(); + } + + AppendOpaque(NGInlineItem::kCloseTag, node); Exit(node); } 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 d82beb0fb5d..39612fa1c01 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 @@ -6,7 +6,6 @@ #include <memory> -#include "third_party/blink/renderer/core/layout/logical_values.h" #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" @@ -295,7 +294,7 @@ void NGInlineLayoutAlgorithm::CreateLine( if (UNLIKELY(Node().IsBidiEnabled())) { box_states_->PrepareForReorder(&line_box_); - BidiReorder(); + BidiReorder(line_info->BaseDirection()); box_states_->UpdateAfterReorder(&line_box_); } LayoutUnit inline_size = box_states_->ComputeInlinePositions(&line_box_); @@ -366,7 +365,6 @@ void NGInlineLayoutAlgorithm::CreateLine( container_builder_.SetIsSelfCollapsing(); container_builder_.SetIsEmptyLineBox(); container_builder_.SetBaseDirection(line_info->BaseDirection()); - container_builder_.AddChildren(line_box_); return; } @@ -380,7 +378,6 @@ void NGInlineLayoutAlgorithm::CreateLine( if (line_info->UseFirstLineStyle()) container_builder_.SetStyleVariant(NGStyleVariant::kFirstLine); container_builder_.SetBaseDirection(line_info->BaseDirection()); - container_builder_.AddChildren(line_box_); container_builder_.SetInlineSize(inline_size); container_builder_.SetMetrics(line_box_metrics); container_builder_.SetBfcBlockOffset(line_info->BfcOffset().block_offset); @@ -763,7 +760,7 @@ LayoutUnit NGInlineLayoutAlgorithm::ComputeContentSize( NGBfcOffset bfc_offset = {ContainerBfcOffset().line_offset, ContainerBfcOffset().block_offset + content_size}; AdjustToClearance( - exclusion_space.ClearanceOffset(ResolvedClear(*item.Style(), Style())), + exclusion_space.ClearanceOffset(item.Style()->Clear(Style())), &bfc_offset); content_size = bfc_offset.block_offset - ContainerBfcOffset().block_offset; } @@ -958,6 +955,22 @@ scoped_refptr<const NGLayoutResult> NGInlineLayoutAlgorithm::Layout() { CHECK(is_line_created); container_builder_.SetExclusionSpace(std::move(exclusion_space)); + + if (NGFragmentItemsBuilder* items_builder = context_->ItemsBuilder()) { + DCHECK(RuntimeEnabledFeatures::LayoutNGFragmentItemEnabled()); + container_builder_.AddOutOfFlowChildren(line_box_); + scoped_refptr<const NGLayoutResult> layout_result = + container_builder_.ToLineBoxFragment(); + if (items_builder->TextContent(false).IsNull()) + items_builder->SetTextContent(Node()); + items_builder->SetCurrentLine( + To<NGPhysicalLineBoxFragment>(layout_result->PhysicalFragment()), + std::move(line_box_)); + return layout_result; + } + + DCHECK(!RuntimeEnabledFeatures::LayoutNGFragmentItemEnabled()); + container_builder_.AddChildren(line_box_); container_builder_.MoveOutOfFlowDescendantCandidatesToDescendants(); return container_builder_.ToLineBoxFragment(); } @@ -968,7 +981,6 @@ unsigned NGInlineLayoutAlgorithm::PositionLeadingFloats( NGExclusionSpace* exclusion_space, NGPositionedFloatVector* positioned_floats) { bool is_empty_inline = Node().IsEmptyInline(); - bool should_ignore_floats = BreakToken() && BreakToken()->IgnoreFloats(); const Vector<NGInlineItem>& items = Node().ItemsData(/* is_first_line */ false).items; @@ -983,12 +995,12 @@ unsigned NGInlineLayoutAlgorithm::PositionLeadingFloats( break; } - if (item.Type() != NGInlineItem::kFloating || should_ignore_floats) + if (item.Type() != NGInlineItem::kFloating) continue; container_builder_.AddAdjoiningObjectTypes( - ResolvedFloating(item.GetLayoutObject()->StyleRef().Floating(), - ConstraintSpace().Direction()) == EFloat::kLeft + item.GetLayoutObject()->StyleRef().Floating( + ConstraintSpace().Direction()) == EFloat::kLeft ? kAdjoiningFloatLeft : kAdjoiningFloatRight); @@ -1022,37 +1034,75 @@ NGPositionedFloat NGInlineLayoutAlgorithm::PositionFloat( &unpositioned_float, ConstraintSpace(), Style(), exclusion_space); } -void NGInlineLayoutAlgorithm::BidiReorder() { +void NGInlineLayoutAlgorithm::BidiReorder(TextDirection base_direction) { + if (line_box_.IsEmpty()) + return; + // TODO(kojii): UAX#9 L1 is not supported yet. Supporting L1 may change // embedding levels of parts of runs, which requires to split items. // http://unicode.org/reports/tr9/#L1 // BidiResolver does not support L1 crbug.com/316409. + // A sentinel value for items that are opaque to bidi reordering. Should be + // larger than the maximum resolved level. + constexpr UBiDiLevel kOpaqueBidiLevel = 0xff; + DCHECK_GT(kOpaqueBidiLevel, UBIDI_MAX_EXPLICIT_LEVEL + 1); + // Create a list of chunk indices in the visual order. // ICU |ubidi_getVisualMap()| works for a run of characters. Since we can // handle the direction of each run, we use |ubidi_reorderVisual()| to reorder // runs instead of characters. - NGLineBoxFragmentBuilder::ChildList logical_items; Vector<UBiDiLevel, 32> levels; - logical_items.ReserveInitialCapacity(line_box_.size()); levels.ReserveInitialCapacity(line_box_.size()); + bool has_opaque_items = false; for (NGLineBoxFragmentBuilder::Child& item : line_box_) { - if (item.IsPlaceholder()) + if (item.IsOpaqueToBidiReordering()) { + levels.push_back(kOpaqueBidiLevel); + has_opaque_items = true; continue; + } + DCHECK_NE(item.bidi_level, kOpaqueBidiLevel); levels.push_back(item.bidi_level); - logical_items.AddChild(std::move(item)); - DCHECK(!item.HasInFlowFragment()); } + // For opaque items, copy bidi levels from adjacent items. + if (has_opaque_items) { + UBiDiLevel last_level = levels.front(); + if (last_level == kOpaqueBidiLevel) { + for (const UBiDiLevel level : levels) { + if (level != kOpaqueBidiLevel) { + last_level = level; + break; + } + } + } + // If all items are opaque, use the base direction. + if (last_level == kOpaqueBidiLevel) { + if (IsLtr(base_direction)) + return; + last_level = 1; + } + for (UBiDiLevel& level : levels) { + if (level == kOpaqueBidiLevel) + level = last_level; + else + last_level = level; + } + } + + // Compute visual indices from resolved levels. Vector<int32_t, 32> indices_in_visual_order(levels.size()); NGBidiParagraph::IndicesInVisualOrder(levels, &indices_in_visual_order); // Reorder to the visual order. - line_box_.resize(0); + NGLineBoxFragmentBuilder::ChildList visual_items; + visual_items.ReserveInitialCapacity(line_box_.size()); for (unsigned logical_index : indices_in_visual_order) { - line_box_.AddChild(std::move(logical_items[logical_index])); - DCHECK(!logical_items[logical_index].HasInFlowFragment()); + visual_items.AddChild(std::move(line_box_[logical_index])); + DCHECK(!line_box_[logical_index].HasInFlowFragment()); } + DCHECK_EQ(line_box_.size(), visual_items.size()); + line_box_ = std::move(visual_items); } } // namespace blink 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 d93ab75c097..4b7c45e4a14 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 @@ -74,7 +74,7 @@ class CORE_EXPORT NGInlineLayoutAlgorithm final const NGInlineItemResult&, NGInlineBoxState*); - void BidiReorder(); + void BidiReorder(TextDirection base_direction); void PlaceControlItem(const NGInlineItem&, const NGLineInfo&, diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm_test.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_inline_layout_algorithm_test.cc index 94c4a3258ac..3c1aa36a1bc 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 @@ -43,12 +43,11 @@ TEST_F(NGInlineLayoutAlgorithmTest, BreakToken) { NGInlineNode inline_node(block_flow); LogicalSize size(LayoutUnit(50), LayoutUnit(20)); - NGConstraintSpace constraint_space = - NGConstraintSpaceBuilder( - WritingMode::kHorizontalTb, WritingMode::kHorizontalTb, - /* is_new_fc */ false) - .SetAvailableSize(size) - .ToConstraintSpace(); + NGConstraintSpaceBuilder builder(WritingMode::kHorizontalTb, + WritingMode::kHorizontalTb, + /* is_new_fc */ false); + builder.SetAvailableSize(size); + NGConstraintSpace constraint_space = builder.ToConstraintSpace(); NGInlineChildLayoutContext context; scoped_refptr<const NGLayoutResult> layout_result = @@ -126,6 +125,38 @@ TEST_F(NGInlineLayoutAlgorithmTest, GenerateEllipsis) { EXPECT_EQ(line1.Children()[0]->GetLayoutObject(), ellipsis.GetLayoutObject()); } +TEST_F(NGInlineLayoutAlgorithmTest, EllipsisInlineBoxOnly) { + LoadAhem(); + SetBodyInnerHTML(R"HTML( + <!DOCTYPE html> + <style> + html, body { margin: 0; } + #container { + font: 10px/1 Ahem; + width: 5ch; + overflow: hidden; + text-overflow: ellipsis; + } + span { + border: solid 10ch blue; + } + </style> + <div id=container><span></span></div> + )HTML"); + scoped_refptr<const NGPhysicalBoxFragment> block = + GetBoxFragmentByElementId("container"); + EXPECT_EQ(1u, block->Children().size()); + const auto& line1 = + To<NGPhysicalLineBoxFragment>(*block->Children()[0].get()); + + // There should not be ellipsis in this line. + for (const auto& child : line1.Children()) { + if (const auto* text = DynamicTo<NGPhysicalTextFragment>(child.get())) { + EXPECT_FALSE(text->IsEllipsis()); + } + } +} + // This test ensures box fragments are generated when necessary, even when the // line is empty. One such case is when the line contains a containing box of an // out-of-flow object. @@ -226,8 +257,8 @@ TEST_F(NGInlineLayoutAlgorithmTest, ContainerBorderPadding) { auto* block_flow = To<LayoutBlockFlow>(GetLayoutObjectByElementId("container")); NGBlockNode block_node(block_flow); - NGConstraintSpace space = - NGConstraintSpace::CreateFromLayoutObject(*block_flow); + NGConstraintSpace space = NGConstraintSpace::CreateFromLayoutObject( + *block_flow, false /* is_layout_root */); scoped_refptr<const NGLayoutResult> layout_result = block_node.Layout(space); EXPECT_TRUE(layout_result->BfcBlockOffset().has_value()); @@ -260,8 +291,8 @@ TEST_F(NGInlineLayoutAlgorithmTest, MAYBE_VerticalAlignBottomReplaced) { To<LayoutBlockFlow>(GetLayoutObjectByElementId("container")); NGInlineNode inline_node(block_flow); NGInlineChildLayoutContext context; - NGConstraintSpace space = - NGConstraintSpace::CreateFromLayoutObject(*block_flow); + NGConstraintSpace space = NGConstraintSpace::CreateFromLayoutObject( + *block_flow, false /* is_layout_root */); scoped_refptr<const NGLayoutResult> layout_result = inline_node.Layout(space, nullptr, &context); 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 912fa83b133..e055e19d365 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 @@ -13,7 +13,6 @@ #include "third_party/blink/renderer/core/layout/layout_list_marker.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/logical_values.h" #include "third_party/blink/renderer/core/layout/ng/inline/layout_ng_text.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_bidi_paragraph.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_dirty_lines.h" @@ -40,6 +39,7 @@ #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_spacing.h" #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_view.h" #include "third_party/blink/renderer/platform/wtf/text/character_names.h" +#include "third_party/blink/renderer/platform/wtf/text/string_buffer.h" namespace blink { @@ -132,6 +132,117 @@ class ItemsBuilderForMarkLineBoxesDirty { bool has_floating_or_out_of_flow_positioned_ = false; }; +// Wrapper over ShapeText that re-uses existing shape results for items that +// haven't changed. +class ReusingTextShaper final { + public: + ReusingTextShaper(NGInlineItemsData* data, + const Vector<NGInlineItem>* reusable_items) + : data_(*data), + reusable_items_(reusable_items), + shaper_(data->text_content) {} + + scoped_refptr<ShapeResult> Shape(const NGInlineItem& start_item, + unsigned end_offset) { + const unsigned start_offset = start_item.StartOffset(); + DCHECK_LT(start_offset, end_offset); + + if (!reusable_items_) + return Reshape(start_item, start_offset, end_offset); + + // TODO(yosin): We should support segment text + if (data_.segments) + return Reshape(start_item, start_offset, end_offset); + + const Vector<const ShapeResult*> reusable_shape_results = + CollectReusableShapeResults(start_offset, end_offset, + start_item.Direction()); + if (reusable_shape_results.IsEmpty()) + return Reshape(start_item, start_offset, end_offset); + + const scoped_refptr<ShapeResult> shape_result = + ShapeResult::CreateEmpty(*reusable_shape_results.front()); + unsigned offset = start_offset; + for (const ShapeResult* reusable_shape_result : reusable_shape_results) { + DCHECK_LE(offset, reusable_shape_result->StartIndex()); + if (offset < reusable_shape_result->StartIndex()) { + AppendShapeResult( + *Reshape(start_item, offset, reusable_shape_result->StartIndex()), + shape_result.get()); + offset = shape_result->EndIndex(); + } + DCHECK_EQ(offset, reusable_shape_result->StartIndex()); + DCHECK(shape_result->NumCharacters() == 0 || + shape_result->EndIndex() == offset); + reusable_shape_result->CopyRange( + offset, std::min(reusable_shape_result->EndIndex(), end_offset), + shape_result.get()); + offset = shape_result->EndIndex(); + if (offset == end_offset) + return shape_result; + } + DCHECK_LT(offset, end_offset); + AppendShapeResult(*Reshape(start_item, offset, end_offset), + shape_result.get()); + return shape_result; + } + + private: + void AppendShapeResult(const ShapeResult& shape_result, ShapeResult* target) { + DCHECK(target->NumCharacters() == 0 || + target->EndIndex() == shape_result.StartIndex()); + shape_result.CopyRange(shape_result.StartIndex(), shape_result.EndIndex(), + target); + } + + Vector<const ShapeResult*> CollectReusableShapeResults( + unsigned start_offset, + unsigned end_offset, + TextDirection direction) { + DCHECK_LT(start_offset, end_offset); + Vector<const ShapeResult*> shape_results; + if (!reusable_items_) + return shape_results; + for (const NGInlineItem *item = std::lower_bound( + reusable_items_->begin(), reusable_items_->end(), start_offset, + [](const NGInlineItem&item, unsigned offset) { + return item.EndOffset() <= offset; + }); + item != reusable_items_->end(); ++item) { + DCHECK_LE(start_offset, item->StartOffset()); + if (end_offset <= item->StartOffset()) + break; + if (item->EndOffset() < start_offset) + continue; + if (!item->TextShapeResult() || item->Direction() != direction) + continue; + shape_results.push_back(item->TextShapeResult()); + } + return shape_results; + } + + scoped_refptr<ShapeResult> Reshape(const NGInlineItem& start_item, + unsigned start_offset, + unsigned end_offset) { + DCHECK_LT(start_offset, end_offset); + const TextDirection direction = start_item.Direction(); + const Font& font = start_item.Style()->GetFont(); + if (data_.segments) { + return data_.segments->ShapeText(&shaper_, &font, direction, start_offset, + end_offset, + &start_item - data_.items.begin()); + } + RunSegmenter::RunSegmenterRange range = + start_item.CreateRunSegmenterRange(); + range.end = end_offset; + return shaper_.Shape(&font, direction, start_offset, end_offset, range); + } + + NGInlineItemsData& data_; + const Vector<NGInlineItem>* const reusable_items_; + HarfBuzzShaper shaper_; +}; + // The function is templated to indicate the purpose of collected inlines: // - With EmptyOffsetMappingBuilder: updating layout; // - With NGOffsetMappingBuilder: building offset mapping on clean layout. @@ -367,7 +478,7 @@ void NGInlineNode::PrepareLayout( DCHECK(data); CollectInlines(data, previous_data.get(), dirty_lines); SegmentText(data); - ShapeText(data, previous_data.get()); + ShapeText(data, &previous_data->text_content); ShapeTextForFirstLineIfNeeded(data); AssociateItemsWithInlines(data); DCHECK_EQ(data, MutableData()); @@ -385,6 +496,288 @@ void NGInlineNode::PrepareLayout( #endif } +// Building |NGInlineNodeData| for |LayoutText::SetTextWithOffset()| with +// reusing data. +class NGInlineNodeDataEditor final { + STACK_ALLOCATED(); + + public: + explicit NGInlineNodeDataEditor(const LayoutText& layout_text) + : block_flow_(layout_text.ContainingNGBlockFlow()), + layout_text_(layout_text) { + DCHECK(layout_text_.HasValidInlineItems()); + } + + LayoutBlockFlow* GetLayoutBlockFlow() const { return block_flow_; } + + // Note: We can't use |Position| for |layout_text_.GetNode()| because |Text| + // node is already changed. + NGInlineNodeData* Prepare(unsigned offset, unsigned length) { + if (!block_flow_ || block_flow_->NeedsCollectInlines() || + block_flow_->NeedsLayout() || + block_flow_->GetDocument().NeedsLayoutTreeUpdate() || + !block_flow_->GetNGInlineNodeData() || + block_flow_->GetNGInlineNodeData()->text_content.IsNull()) + return nullptr; + + // Because of current text content has secured text, e.g. whole text is + // "***", all characters including collapsed white spaces are marker, and + // new text is original text, we can't reuse shape result. + if (layout_text_.StyleRef().TextSecurity() != ETextSecurity::kNone) + return nullptr; + + // Note: We should compute offset mapping before calling + // |LayoutBlockFlow::TakeNGInlineNodeData()| + const NGOffsetMapping* const offset_mapping = + NGInlineNode::GetOffsetMapping(block_flow_); + DCHECK(offset_mapping); + const auto units = + offset_mapping->GetMappingUnitsForLayoutObject(layout_text_); + start_offset_ = ConvertDOMOffsetToTextContent(units, offset); + end_offset_ = ConvertDOMOffsetToTextContent(units, offset + length); + DCHECK_LE(start_offset_, end_offset_); + data_.reset(block_flow_->TakeNGInlineNodeData()); + return data_.get(); + } + + void Run() { + const NGInlineNodeData& new_data = *block_flow_->GetNGInlineNodeData(); + const int diff = + new_data.text_content.length() - data_->text_content.length(); + // |inserted_text_length| can be negative when white space is collapsed + // after text change. + // * "ab cd ef" => delete "cd" => "ab ef" + // We should not reuse " " before "ef" + // * "a bc" => delete "bc" => "a" + // There are no spaces after "a". + const int inserted_text_length = end_offset_ - start_offset_ + diff; + DCHECK_GE(inserted_text_length, -1); + const unsigned start_offset = + inserted_text_length < 0 && end_offset_ == data_->text_content.length() + ? start_offset_ - 1 + : start_offset_; + const unsigned end_offset = + inserted_text_length < 0 && start_offset_ == start_offset + ? end_offset_ + 1 + : end_offset_; + DCHECK_LE(end_offset, data_->text_content.length()); + DCHECK_LE(start_offset, end_offset); +#if DCHECK_IS_ON() + if (start_offset_ != start_offset) { + DCHECK_EQ(data_->text_content[start_offset], ' '); + DCHECK_EQ(end_offset, end_offset_); + } + if (end_offset_ != end_offset) { + DCHECK_EQ(data_->text_content[end_offset_], ' '); + DCHECK_EQ(start_offset, start_offset_); + } +#endif + Vector<NGInlineItem> items; + // +3 for before and after replaced text. + items.ReserveInitialCapacity(data_->items.size() + 3); + + // Copy items before replaced range + auto* it = data_->items.begin(); + while (it->end_offset_ < start_offset || + it->layout_object_ != layout_text_) { + DCHECK(it != data_->items.end()); + items.push_back(*it); + ++it; + } + + DCHECK_EQ(it->layout_object_, layout_text_); + + // Copy part of item before replaced range. + if (it->start_offset_ < start_offset) + items.push_back(CopyItemBefore(*it, start_offset)); + + // Skip items in replaced range. + while (it->end_offset_ < end_offset) + ++it; + DCHECK_EQ(it->layout_object_, layout_text_); + + // Inserted text + if (inserted_text_length > 0) { + const unsigned inserted_start_offset = + items.IsEmpty() ? 0 : items.back().end_offset_; + const unsigned inserted_end_offset = + inserted_start_offset + inserted_text_length; + items.push_back(NGInlineItem(*it, inserted_start_offset, + inserted_end_offset, nullptr)); + } + + // Copy part of item after replaced range. + if (end_offset < it->end_offset_) { + items.push_back(CopyItemAfter(*it, end_offset)); + ShiftItem(&items.back(), diff); + } + + // Copy items after replaced range + ++it; + while (it != data_->items.end()) { + DCHECK_NE(it->layout_object_, layout_text_); + DCHECK_LE(end_offset, it->start_offset_); + items.push_back(*it); + ShiftItem(&items.back(), diff); + ++it; + } + + VerifyItems(items); + data_->items = std::move(items); + data_->text_content = new_data.text_content; + } + + private: + static unsigned AdjustOffset(unsigned offset, int delta) { + if (delta > 0) + return offset + delta; + return offset - (-delta); + } + + static unsigned ConvertDOMOffsetToTextContent( + base::span<const NGOffsetMappingUnit> units, + unsigned offset) { + auto it = std::find_if( + units.begin(), units.end(), [offset](const NGOffsetMappingUnit& unit) { + return unit.DOMStart() <= offset && offset <= unit.DOMEnd(); + }); + DCHECK(it != units.end()); + return it->ConvertDOMOffsetToTextContent(offset); + } + + // Returns copy of |item| after |start_offset| (inclusive). + NGInlineItem CopyItemAfter(const NGInlineItem& item, + unsigned start_offset) const { + DCHECK_LE(item.start_offset_, start_offset); + DCHECK_LT(start_offset, item.end_offset_); + DCHECK_EQ(item.layout_object_, layout_text_); + if (item.start_offset_ == start_offset) + return item; + const unsigned end_offset = item.end_offset_; + if (!item.shape_result_) + return NGInlineItem(item, start_offset, end_offset, nullptr); + // TODO(yosin): We should handle |shape_result| doesn't have safe-to-break + // at start and end, because of |ShapeText()| splits |ShapeResult| ignoring + // safe-to-break offset. + item.shape_result_->EnsurePositionData(); + const unsigned safe_start_offset = + item.shape_result_->CachedNextSafeToBreakOffset(start_offset); + if (end_offset == safe_start_offset) + return NGInlineItem(item, start_offset, end_offset, nullptr); + return NGInlineItem( + item, start_offset, end_offset, + item.shape_result_->SubRange(safe_start_offset, end_offset)); + } + + // Returns copy of |item| before |end_offset| (exclusive). + NGInlineItem CopyItemBefore(const NGInlineItem& item, + unsigned end_offset) const { + DCHECK_LT(item.start_offset_, end_offset); + DCHECK_LE(end_offset, item.end_offset_); + DCHECK_EQ(item.layout_object_, layout_text_); + if (item.end_offset_ == end_offset) + return item; + const unsigned start_offset = item.start_offset_; + if (!item.shape_result_) + return NGInlineItem(item, start_offset, end_offset, nullptr); + // TODO(yosin): We should handle |shape_result| doesn't have safe-to-break + // at start and end, because of |ShapeText()| splits |ShapeResult| ignoring + // safe-to-break offset. + item.shape_result_->EnsurePositionData(); + const unsigned safe_end_offset = + item.shape_result_->CachedPreviousSafeToBreakOffset(end_offset); + if (start_offset == safe_end_offset) + return NGInlineItem(item, start_offset, end_offset, nullptr); + return NGInlineItem( + item, start_offset, end_offset, + item.shape_result_->SubRange(start_offset, safe_end_offset)); + } + + static void ShiftItem(NGInlineItem* item, int delta) { + if (delta == 0) + return; + item->start_offset_ = AdjustOffset(item->start_offset_, delta); + item->end_offset_ = AdjustOffset(item->end_offset_, delta); + if (!item->shape_result_) + return; + item->shape_result_ = + item->shape_result_->CopyAdjustedOffset(item->start_offset_); + } + + void VerifyItems(const Vector<NGInlineItem>& items) const { +#if DCHECK_IS_ON() + unsigned last_offset = items.front().start_offset_; + for (const NGInlineItem& item : items) { + DCHECK_LE(item.start_offset_, item.end_offset_); + DCHECK_EQ(last_offset, item.start_offset_); + last_offset = item.end_offset_; + if (!item.shape_result_ || item.layout_object_ != layout_text_) + continue; + DCHECK_LT(item.start_offset_, item.end_offset_); + if (item.shape_result_->StartIndex() == item.start_offset_) { + DCHECK_LE(item.shape_result_->EndIndex(), item.end_offset_); + } else { + DCHECK_LE(item.start_offset_, item.shape_result_->StartIndex()); + DCHECK_EQ(item.end_offset_, item.shape_result_->EndIndex()); + } + } + DCHECK_EQ(last_offset, + block_flow_->GetNGInlineNodeData()->text_content.length()); +#endif + } + + std::unique_ptr<NGInlineNodeData> data_; + LayoutBlockFlow* const block_flow_; + const LayoutText& layout_text_; + unsigned start_offset_ = 0; + unsigned end_offset_ = 0; + + DISALLOW_COPY_AND_ASSIGN(NGInlineNodeDataEditor); +}; + +// static +bool NGInlineNode::SetTextWithOffset(LayoutText* layout_text, + scoped_refptr<StringImpl> new_text_in, + unsigned offset, + unsigned length) { + if (!layout_text->HasValidInlineItems() || + !layout_text->IsInLayoutNGInlineFormattingContext()) + return false; + const String old_text = layout_text->GetText(); + if (offset == 0 && length == old_text.length()) { + // We'll run collect inline items since whole text of |layout_text| is + // changed. + return false; + } + + NGInlineNodeDataEditor editor(*layout_text); + NGInlineNodeData* const previous_data = editor.Prepare(offset, length); + if (!previous_data) + return false; + + String new_text(std::move(new_text_in)); + layout_text->StyleRef().ApplyTextTransform(&new_text, + layout_text->PreviousCharacter()); + layout_text->SetTextInternal(new_text.Impl()); + + NGInlineNode node(editor.GetLayoutBlockFlow()); + NGInlineNodeData* data = node.MutableData(); + data->items.ReserveCapacity(previous_data->items.size()); + NGInlineItemsBuilder builder(&data->items, nullptr); + // TODO(yosin): We should reuse before/after |layout_text| during collecting + // inline items. + layout_text->ClearInlineItems(); + CollectInlinesInternal(node.GetLayoutBlockFlow(), &builder, previous_data); + data->text_content = builder.ToString(); + // Relocates |ShapeResult| in |previous_data| after |offset|+|length| + editor.Run(); + node.SegmentText(data); + node.ShapeText(data, &previous_data->text_content, &previous_data->items); + node.ShapeTextForFirstLineIfNeeded(data); + node.AssociateItemsWithInlines(data); + return true; +} + const NGInlineNodeData& NGInlineNode::EnsureData() { PrepareLayoutIfNeeded(); return Data(); @@ -559,22 +952,26 @@ void NGInlineNode::SegmentFontOrientation(NGInlineNodeData* data) { // If we don't have |NGInlineItemSegments| yet, create a segment for the // entire content. const unsigned capacity = items.size() + text_content.length() / 10; - if (!data->segments) { - data->segments = std::make_unique<NGInlineItemSegments>(); - data->segments->ReserveCapacity(capacity); - data->segments->Append(text_content.length(), items.front()); - } else { + NGInlineItemSegments* segments = data->segments.get(); + if (segments) { DCHECK(!data->segments->IsEmpty()); data->segments->ReserveCapacity(capacity); + DCHECK_EQ(text_content.length(), data->segments->EndOffset()); } - DCHECK_EQ(text_content.length(), data->segments->EndOffset()); unsigned segment_index = 0; for (const NGInlineItem& item : items) { if (item.Type() == NGInlineItem::kText && item.Length() && item.Style()->GetFont().GetFontDescription().Orientation() == FontOrientation::kVerticalMixed) { - segment_index = data->segments->AppendMixedFontOrientation( + if (!segments) { + data->segments = std::make_unique<NGInlineItemSegments>(); + segments = data->segments.get(); + segments->ReserveCapacity(capacity); + segments->Append(text_content.length(), item); + DCHECK_EQ(text_content.length(), data->segments->EndOffset()); + } + segment_index = segments->AppendMixedFontOrientation( text_content, item.StartOffset(), item.EndOffset(), segment_index); } } @@ -626,14 +1023,13 @@ void NGInlineNode::SegmentBidiRuns(NGInlineNodeData* data) { } void NGInlineNode::ShapeText(NGInlineItemsData* data, - NGInlineItemsData* previous_data) { + const String* previous_text, + const Vector<NGInlineItem>* previous_items) { const String& text_content = data->text_content; Vector<NGInlineItem>* items = &data->items; - const String* previous_text = - previous_data ? &previous_data->text_content : nullptr; // Provide full context of the entire node to the shaper. - HarfBuzzShaper shaper(text_content); + ReusingTextShaper shaper(data, previous_items); ShapeResultSpacing<String> spacing(text_content); DCHECK(!data->segments || @@ -718,43 +1114,35 @@ void NGInlineNode::ShapeText(NGInlineItemsData* data, } // 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; + if (previous_text) { + 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; + // 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; - if (!data->segments) { - RunSegmenter::RunSegmenterRange range = - start_item.CreateRunSegmenterRange(); - range.end = end_offset; - shape_result = shaper.Shape(&font, direction, start_item.StartOffset(), - end_offset, range); - } else { - shape_result = data->segments->ShapeText( - &shaper, &font, direction, start_item.StartOffset(), end_offset, - &start_item - items->begin()); - } + scoped_refptr<ShapeResult> shape_result = + shaper.Shape(start_item, end_offset); if (UNLIKELY(spacing.SetSpacing(font.GetFontDescription()))) shape_result->ApplySpacing(spacing); @@ -1044,6 +1432,50 @@ bool NGInlineNode::MarkLineBoxesDirty(LayoutBlockFlow* block_flow, return !builder.ShouldAbort(); } +namespace { + +template <typename CharType> +scoped_refptr<StringImpl> CreateTextContentForStickyImagesQuirk( + const CharType* text, + unsigned length, + base::span<const NGInlineItem> items) { + StringBuffer<CharType> buffer(length); + CharType* characters = buffer.Characters(); + memcpy(characters, text, length * sizeof(CharType)); + for (const NGInlineItem& item : items) { + if (item.Type() == NGInlineItem::kAtomicInline && item.IsImage()) { + DCHECK_EQ(characters[item.StartOffset()], kObjectReplacementCharacter); + characters[item.StartOffset()] = kNoBreakSpaceCharacter; + } + } + return buffer.Release(); +} + +} // namespace + +// The stick images quirk changes the line breaking behavior around images. This +// function returns a text content that has non-breaking spaces for images, so +// that no changes are needed in the line breaking logic. +// https://quirks.spec.whatwg.org/#the-table-cell-width-calculation-quirk +// static +String NGInlineNode::TextContentForStickyImagesQuirk( + const NGInlineItemsData& items_data) { + const String& text_content = items_data.text_content; + for (const NGInlineItem& item : items_data.items) { + if (item.Type() == NGInlineItem::kAtomicInline && item.IsImage()) { + if (text_content.Is8Bit()) { + return CreateTextContentForStickyImagesQuirk( + text_content.Characters8(), text_content.length(), + base::span<const NGInlineItem>(&item, items_data.items.end())); + } + return CreateTextContentForStickyImagesQuirk( + text_content.Characters16(), text_content.length(), + base::span<const NGInlineItem>(&item, items_data.items.end())); + } + } + return text_content; +} + static LayoutUnit ComputeContentSize( NGInlineNode node, WritingMode container_writing_mode, @@ -1056,16 +1488,15 @@ static LayoutUnit ComputeContentSize( LayoutUnit available_inline_size = mode == NGLineBreakerMode::kMaxContent ? LayoutUnit::Max() : LayoutUnit(); - NGConstraintSpace space = - NGConstraintSpaceBuilder(/* parent_writing_mode */ writing_mode, - /* out_writing_mode */ writing_mode, - /* is_new_fc */ false) - .SetTextDirection(style.Direction()) - .SetAvailableSize({available_inline_size, kIndefiniteSize}) - .SetPercentageResolutionSize({LayoutUnit(), LayoutUnit()}) - .SetReplacedPercentageResolutionSize({LayoutUnit(), LayoutUnit()}) - .SetIsIntermediateLayout(true) - .ToConstraintSpace(); + NGConstraintSpaceBuilder builder(/* parent_writing_mode */ writing_mode, + /* out_writing_mode */ writing_mode, + /* is_new_fc */ false); + builder.SetTextDirection(style.Direction()); + builder.SetAvailableSize({available_inline_size, kIndefiniteSize}); + builder.SetPercentageResolutionSize({LayoutUnit(), LayoutUnit()}); + builder.SetReplacedPercentageResolutionSize({LayoutUnit(), LayoutUnit()}); + builder.SetIsIntermediateLayout(true); + NGConstraintSpace space = builder.ToConstraintSpace(); NGExclusionSpace empty_exclusion_space; NGPositionedFloatVector empty_leading_floats; @@ -1104,7 +1535,7 @@ static LayoutUnit ComputeContentSize( EFloat previous_float_type = EFloat::kNone; for (const auto& floating_object : floating_objects_) { const EClear float_clear = - ResolvedClear(floating_object.float_style, floating_object.style); + floating_object.float_style.Clear(floating_object.style); // If this float clears the previous float we start a new "line". // This is subtly different to block layout which will only reset either @@ -1122,8 +1553,8 @@ static LayoutUnit ComputeContentSize( // such float should not affect the content size. floats_inline_size_ += floating_object.float_inline_max_size_with_margin .ClampNegativeToZero(); - previous_float_type = ResolvedFloating(floating_object.float_style, - floating_object.style); + previous_float_type = + floating_object.float_style.Floating(floating_object.style); } max_inline_size = std::max(max_inline_size, line_inline_size + floats_inline_size_); @@ -1179,11 +1610,11 @@ static LayoutUnit ComputeContentSize( } } - void ForceLineBreak(const NGInlineItem& item) { - // Add all text up to the forced break. There may be spaces that were + void ForceLineBreak(const NGLineInfo& line_info) { + // Add all text up to the end of the line. There may be spaces that were // removed during the line breaking. - AddTextUntil(&item); - next_item = std::next(&item); + CHECK_LE(line_info.EndItemIndex(), items_data.items.size()); + AddTextUntil(items_data.items.begin() + line_info.EndItemIndex()); max_size = floats->ComputeMaxSizeForLine(position.ClampNegativeToZero(), max_size); position = LayoutUnit(); @@ -1217,6 +1648,7 @@ static LayoutUnit ComputeContentSize( is_after_break = false; } + bool has_forced_break = false; for (const NGInlineItemResult& result : line_info.Results()) { const NGInlineItem& item = *result.item; if (item.Type() == NGInlineItem::kText) { @@ -1234,7 +1666,11 @@ static LayoutUnit ComputeContentSize( if (item.Type() == NGInlineItem::kControl) { UChar c = items_data.text_content[item.StartOffset()]; if (c == kNewlineCharacter) { - ForceLineBreak(item); + // Compute the forced break after all results were handled, because + // when close tags appear after a forced break, they are included in + // the line, and they may have inline sizes. crbug.com/991320. + DCHECK(!has_forced_break); + has_forced_break = true; continue; } // Tabulation characters change the widths by their positions, so @@ -1247,6 +1683,8 @@ static LayoutUnit ComputeContentSize( } position += result.inline_size; } + if (has_forced_break) + ForceLineBreak(line_info); } }; FloatsMaxSize floats_max_size(input); 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 e9f6f1031aa..318a6cefa28 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 @@ -72,12 +72,26 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode { return Data().ItemsData(is_first_line); } + // Returns the text content to use for content sizing. This is normally the + // same as |items_data.text_content|, except when sticky images quirk is + // needed. + String TextContentForContentSize(const NGInlineItemsData& items_data) const; + // Clear associated fragments for LayoutObjects. // They are associated when NGPaintFragment is constructed, but when clearing, // NGInlineItem provides easier and faster logic. static void ClearAssociatedFragments(const NGPhysicalFragment& fragment, const NGBlockBreakToken* break_token); + // Returns true if we don't need to collect inline items after replacing + // |layout_text| after deleting replacing subtext from |offset| to |length| + // |new_text| is new text of |layout_text|. + // This is optimized version of |PrepareLayout()|. + static bool SetTextWithOffset(LayoutText* layout_text, + scoped_refptr<StringImpl> new_text, + unsigned offset, + unsigned length); + // 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. @@ -124,7 +138,8 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode { void SegmentFontOrientation(NGInlineNodeData*); void SegmentBidiRuns(NGInlineNodeData*); void ShapeText(NGInlineItemsData*, - NGInlineItemsData* previous_data = nullptr); + const String* previous_text = nullptr, + const Vector<NGInlineItem>* previous_items = nullptr); void ShapeTextForFirstLineIfNeeded(NGInlineNodeData*); void AssociateItemsWithInlines(NGInlineNodeData*); @@ -145,6 +160,8 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode { } const NGInlineNodeData& EnsureData(); + static String TextContentForStickyImagesQuirk(const NGInlineItemsData&); + static void ComputeOffsetMapping(LayoutBlockFlow* layout_block_flow, NGInlineNodeData* data); @@ -152,6 +169,30 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode { friend class NGInlineNodeLegacy; }; +inline String NGInlineNode::TextContentForContentSize( + const NGInlineItemsData& items_data) const { + const String& text_content = items_data.text_content; + if (UNLIKELY(text_content.IsEmpty())) + return text_content; + + // There's a special intrinsic size measure quirk for images that are direct + // children of table cells that have auto inline-size: When measuring + // intrinsic min/max inline sizes, we pretend that it's not possible to break + // between images, or between text and images. Note that this only applies + // when measuring. During actual layout, on the other hand, standard breaking + // rules are to be followed. + // See https://quirks.spec.whatwg.org/#the-table-cell-width-calculation-quirk + if (UNLIKELY(GetDocument().InQuirksMode())) { + const ComputedStyle& style = Style(); + if (UNLIKELY(style.Display() == EDisplay::kTableCell && + style.LogicalWidth().IsIntrinsicOrAuto())) { + return TextContentForStickyImagesQuirk(items_data); + } + } + + return text_content; +} + template <> struct DowncastTraits<NGInlineNode> { static bool AllowFrom(const NGLayoutInputNode& node) { 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 2e844d26617..2d316800b0c 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 @@ -124,12 +124,11 @@ class NGInlineNodeTest : public NGLayoutTest { void CreateLine( NGInlineNode node, Vector<scoped_refptr<const NGPhysicalTextFragment>>* fragments_out) { - NGConstraintSpace constraint_space = - NGConstraintSpaceBuilder(WritingMode::kHorizontalTb, - WritingMode::kHorizontalTb, - /* is_new_fc */ false) - .SetAvailableSize({LayoutUnit::Max(), LayoutUnit(-1)}) - .ToConstraintSpace(); + NGConstraintSpaceBuilder builder(WritingMode::kHorizontalTb, + WritingMode::kHorizontalTb, + /* is_new_fc */ false); + builder.SetAvailableSize({LayoutUnit::Max(), LayoutUnit(-1)}); + NGConstraintSpace constraint_space = builder.ToConstraintSpace(); NGInlineChildLayoutContext context; scoped_refptr<const NGLayoutResult> result = NGInlineLayoutAlgorithm(node, constraint_space, @@ -503,6 +502,25 @@ TEST_F(NGInlineNodeTest, MinMaxSizeFloats) { EXPECT_EQ(130, sizes.max_size); } +TEST_F(NGInlineNodeTest, MinMaxSizeCloseTagAfterForcedBreak) { + LoadAhem(); + SetupHtml("t", R"HTML( + <style> + span { border: 30px solid blue; } + </style> + <div id=t style="font: 10px Ahem"> + <span>12<br></span> + </div> + )HTML"); + + NGInlineNodeForTest node = CreateInlineNode(); + MinMaxSize sizes = ComputeMinMaxSize(node); + // The right border of the `</span>` is included in the line even if it + // appears after `<br>`. crbug.com/991320. + EXPECT_EQ(80, sizes.min_size); + EXPECT_EQ(80, sizes.max_size); +} + TEST_F(NGInlineNodeTest, MinMaxSizeFloatsClearance) { LoadAhem(); SetupHtml("t", R"HTML( @@ -923,16 +941,7 @@ TEST_F(NGInlineNodeTest, InvalidateSetText) { EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines()); LayoutText* text = ToLayoutText(layout_block_flow_->FirstChild()); - text->SetText(String("after").Impl()); - EXPECT_TRUE(layout_block_flow_->NeedsCollectInlines()); -} - -TEST_F(NGInlineNodeTest, InvalidateSetTextWithOffset) { - SetupHtml("t", "<div id=t>before</div>"); - EXPECT_FALSE(layout_block_flow_->NeedsCollectInlines()); - - LayoutText* text = ToLayoutText(layout_block_flow_->FirstChild()); - text->SetTextWithOffset(String("after").Impl(), 1, 4); + text->SetTextIfNeeded(String("after").Impl()); EXPECT_TRUE(layout_block_flow_->NeedsCollectInlines()); } 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 55cb128a930..206b6163dbc 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 @@ -108,6 +108,20 @@ void NGLineBoxFragmentBuilder::AddChildren(ChildList& children) { } } +void NGLineBoxFragmentBuilder::AddOutOfFlowChildren(ChildList& children) { + for (auto& child : children) { + if (child.out_of_flow_positioned_box) { + AddOutOfFlowChildCandidate( + NGBlockNode(ToLayoutBox(child.out_of_flow_positioned_box)), + child.offset, child.container_direction); + child.out_of_flow_positioned_box = nullptr; + } + } + + DCHECK(oof_positioned_descendants_.IsEmpty()); + MoveOutOfFlowDescendantCandidatesToDescendants(); +} + scoped_refptr<const NGLayoutResult> NGLineBoxFragmentBuilder::ToLineBoxFragment() { writing_mode_ = ToLineWritingMode(writing_mode_); 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 7142082a860..80aaf47eba1 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 @@ -91,6 +91,10 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final // The index of |box_data_list_|, used in |PrepareForReorder()| and // |UpdateAfterReorder()| to track children of boxes across BiDi reorder. unsigned box_data_index = 0; + // For an inline box, shows the number of descendant |Child|ren, including + // empty ones. Includes itself, so 1 means no descendants. 0 if not an + // inline box. Available only after |CreateBoxFragments()|. + unsigned children_count = 0; UBiDiLevel bidi_level = 0xff; // The current text direction for OOF positioned items. TextDirection container_direction = TextDirection::kLtr; @@ -162,6 +166,20 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final } bool HasBidiLevel() const { return bidi_level != 0xff; } bool IsPlaceholder() const { return !HasFragment() && !HasBidiLevel(); } + bool IsOpaqueToBidiReordering() const { + if (IsPlaceholder()) + return true; + // Skip all inline boxes. Fragments for inline boxes maybe created earlier + // if they have no children. + if (layout_result) { + const LayoutObject* layout_object = + layout_result->PhysicalFragment().GetLayoutObject(); + DCHECK(layout_object); + if (layout_object->IsLayoutInline()) + return true; + } + return false; + } const NGPhysicalFragment* PhysicalFragment() const { if (layout_result) return &layout_result->PhysicalFragment(); @@ -177,7 +195,7 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final public: ChildList() = default; - void operator=(ChildList&& other) { + void operator=(ChildList&& other) noexcept { children_ = std::move(other.children_); } @@ -235,6 +253,11 @@ class CORE_EXPORT NGLineBoxFragmentBuilder final // Add all items in ChildList. Skips null Child if any. void AddChildren(ChildList&); + // Add only out-of-flow items in ChildList. TODO(kojii): When |NGFragmentItem| + // is on, all objects should go to |NGFragmentItems| but OOF still uses + // fragments to propagate while in transition. + void AddOutOfFlowChildren(ChildList&); + // Creates the fragment. Can only be called once. scoped_refptr<const NGLayoutResult> ToLineBoxFragment(); 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 e3dfe0194be..c3cd893ac35 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.cc @@ -4,7 +4,6 @@ #include "third_party/blink/renderer/core/layout/ng/inline/ng_line_breaker.h" -#include "third_party/blink/renderer/core/layout/logical_values.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_bidi_paragraph.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_break_token.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_node.h" @@ -44,13 +43,6 @@ inline bool HasUnpositionedFloats(const NGInlineItemResults& item_results) { return !item_results.IsEmpty() && item_results.back().has_unpositioned_floats; } -bool IsImage(const NGInlineItem& item) { - if (!item.GetLayoutObject() || !item.GetLayoutObject()->IsLayoutImage()) - return false; - DCHECK(item.Type() == NGInlineItem::kAtomicInline); - return true; -} - LayoutUnit ComputeInlineEndSize(const NGConstraintSpace& space, const ComputedStyle* style) { DCHECK(style); @@ -131,26 +123,6 @@ scoped_refptr<const NGPhysicalTextFragment> CreateHyphenFragment( return builder.ToTextFragment(); } -void PreventBreakBeforeStickyImage( - NGLineBreaker::WhitespaceState trailing_whitespace, - const String& text, - NGLineInfo* line_info) { - if (trailing_whitespace != NGLineBreaker::WhitespaceState::kNone && - trailing_whitespace != NGLineBreaker::WhitespaceState::kUnknown) - return; - - NGInlineItemResults* results = line_info->MutableResults(); - if (results->IsEmpty()) - return; - - // If this image follows a <wbr> the image isn't sticky. - NGInlineItemResult* last = &results->back(); - if (text[last->start_offset] == kZeroWidthSpaceCharacter) - return; - - last->can_break_after = false; -} - inline void ClearNeedsLayout(const NGInlineItem& item) { LayoutObject* layout_object = item.GetLayoutObject(); if (layout_object->NeedsLayout()) @@ -177,6 +149,7 @@ NGLineBreaker::NGLineBreaker(NGInlineNode node, NGExclusionSpace* exclusion_space) : line_opportunity_(line_opportunity), node_(node), + mode_(mode), is_first_formatted_line_((!break_token || (!break_token->ItemIndex() && !break_token->TextOffset())) && node.CanContainFirstFormattedLine()), @@ -184,13 +157,15 @@ NGLineBreaker::NGLineBreaker(NGInlineNode node, node.UseFirstLineStyle()), in_line_height_quirks_mode_(node.InLineHeightQuirksMode()), items_data_(node.ItemsData(use_first_line_style_)), - mode_(mode), + text_content_(mode == NGLineBreakerMode::kContent + ? items_data_.text_content + : node.TextContentForContentSize(items_data_)), constraint_space_(space), exclusion_space_(exclusion_space), break_token_(break_token), - break_iterator_(items_data_.text_content), - shaper_(items_data_.text_content), - spacing_(items_data_.text_content), + break_iterator_(text_content_), + shaper_(text_content_), + spacing_(text_content_), leading_floats_(leading_floats), handled_leading_floats_index_(handled_leading_floats_index), base_direction_(node_.BaseDirection()) { @@ -198,27 +173,15 @@ NGLineBreaker::NGLineBreaker(NGInlineNode node, break_iterator_.SetBreakSpace(BreakSpaceType::kBeforeSpaceRun); if (break_token) { - current_style_ = break_token->Style(); item_index_ = break_token->ItemIndex(); offset_ = break_token->TextOffset(); break_iterator_.SetStartOffset(offset_); is_after_forced_break_ = break_token->IsForcedBreak(); items_data_.AssertOffset(item_index_, offset_); - ignore_floats_ = break_token->IgnoreFloats(); + // TODO(crbug.com/1013040): |break_token->Style()| should not be nullptr. + if (const ComputedStyle* line_initial_style = break_token->Style()) + SetCurrentStyle(*line_initial_style); } - - // There's a special intrinsic size measure quirk for images that are direct - // children of table cells that have auto inline-size: When measuring - // intrinsic min/max inline sizes, we pretend that it's not possible to break - // between images, or between text and images. Note that this only applies - // when measuring. During actual layout, on the other hand, standard breaking - // rules are to be followed. - // See https://quirks.spec.whatwg.org/#the-table-cell-width-calculation-quirk - if (node.GetDocument().InQuirksMode() && - node.Style().Display() == EDisplay::kTableCell && - node.Style().LogicalWidth().IsIntrinsicOrAuto() && - mode != NGLineBreakerMode::kContent) - sticky_images_quirk_ = true; } // Define the destructor here, so that we can forward-declare more in the @@ -313,11 +276,13 @@ void NGLineBreaker::PrepareNextLine(NGLineInfo* line_info) { line_info->SetTextIndent(MinimumValueForLength(length, maximum_value)); } - // Set the initial style of this line from the break token. Example: + // Set the initial style of this line from the line style, if the style from + // the end of previous line is not available. Example: // <p>...<span>....</span></p> // When the line wraps in <span>, the 2nd line needs to start with the style // of the <span>. - SetCurrentStyle(current_style_ ? *current_style_ : line_info->LineStyle()); + if (!current_style_) + SetCurrentStyle(line_info->LineStyle()); ComputeBaseDirection(); line_info->SetBaseDirection(base_direction_); @@ -414,13 +379,6 @@ void NGLineBreaker::BreakLine( continue; return; } - if (item.Type() == NGInlineItem::kAtomicInline) { - if (HandleAtomicInline(item, percentage_resolution_block_size_for_min_max, - line_info)) { - continue; - } - return; - } if (item.Type() == NGInlineItem::kCloseTag) { HandleCloseTag(item, line_info); continue; @@ -446,8 +404,14 @@ void NGLineBreaker::BreakLine( return; } + if (item.Type() == NGInlineItem::kAtomicInline) { + HandleAtomicInline(item, percentage_resolution_block_size_for_min_max, + line_info); + continue; + } if (item.Type() == NGInlineItem::kOutOfFlowPositioned) { - AddItem(item, line_info); + NGInlineItemResult* item_result = AddItem(item, line_info); + ComputeCanBreakAfter(item_result, auto_wrap_, break_iterator_); MoveToNextOf(item); } else if (item.Length()) { NOTREACHED(); @@ -473,7 +437,10 @@ void NGLineBreaker::ComputeLineLocation(NGLineInfo* line_info) const { // Negative margins can make the position negative, but the inline size is // always positive or 0. LayoutUnit available_width = AvailableWidth(); - DCHECK_EQ(position_, line_info->ComputeWidth()); + + // Text measurement is done using floats which may introduce small rounding + // errors for near-saturated values. + DCHECK_EQ(position_.Round(), line_info->ComputeWidth().Round()); line_info->SetWidth(available_width, position_); line_info->SetBfcOffset( @@ -482,6 +449,49 @@ void NGLineBreaker::ComputeLineLocation(NGLineInfo* line_info) const { line_info->UpdateTextAlign(); } +// For Web-compatibility, allow break between an atomic inline and any adjacent +// U+00A0 NO-BREAK SPACE character. +// https://www.w3.org/TR/css-text-3/#line-break-details +bool NGLineBreaker::IsAtomicInlineBeforeNoBreakSpace( + const NGInlineItemResult& item_result) const { + DCHECK(auto_wrap_); + DCHECK_EQ(item_result.item->Type(), NGInlineItem::kAtomicInline); + const String& text = Text(); + DCHECK_GE(text.length(), item_result.end_offset); + return text.length() > item_result.end_offset && + text[item_result.end_offset] == kNoBreakSpaceCharacter && + // Except when sticky images quirk was applied. + text[item_result.start_offset] != kNoBreakSpaceCharacter; +} + +bool NGLineBreaker::IsAtomicInlineAfterNoBreakSpace( + const NGInlineItemResult& item_result) const { + DCHECK(auto_wrap_); + DCHECK_EQ(item_result.item->Type(), NGInlineItem::kText); + const String& text = Text(); + DCHECK_GE(text.length(), item_result.end_offset); + if (text[item_result.end_offset - 1] != kNoBreakSpaceCharacter || + text.length() <= item_result.end_offset || + text[item_result.end_offset] != kObjectReplacementCharacter) + return false; + // This kObjectReplacementCharacter can be any objects, such as a floating or + // an OOF object. Check if it's really an atomic inline. + const Vector<NGInlineItem>& items = Items(); + for (const NGInlineItem* item = std::next(item_result.item); + item != items.end(); ++item) { + DCHECK_EQ(item->StartOffset(), item_result.end_offset); + if (item->Type() == NGInlineItem::kAtomicInline) { + // Except when sticky images quirk was applied. + if (UNLIKELY(text[item->StartOffset()] == kNoBreakSpaceCharacter)) + return false; + return true; + } + if (item->EndOffset() > item_result.end_offset) + break; + } + return false; +} + void NGLineBreaker::HandleText(const NGInlineItem& item, const ShapeResult& shape_result, NGLineInfo* line_info) { @@ -542,7 +552,6 @@ void NGLineBreaker::HandleText(const NGInlineItem& item, !override_break_anywhere_)); position_ += item_result->inline_size; DCHECK_EQ(break_result == kSuccess, position_ <= available_width); - item_result->may_break_inside = break_result == kSuccess; MoveToNextOf(*item_result); if (break_result == kSuccess || @@ -640,12 +649,12 @@ NGLineBreaker::BreakResult NGLineBreaker::BreakText( unsigned try_count = 0; #endif LayoutUnit inline_size; + ShapingLineBreaker::Result result; while (true) { #if DCHECK_IS_ON() ++try_count; DCHECK_LE(try_count, 2u); #endif - ShapingLineBreaker::Result result; scoped_refptr<const ShapeResultView> shape_result = breaker.ShapeLine( item_result->start_offset, available_width.ClampNegativeToZero(), options, &result); @@ -710,8 +719,18 @@ NGLineBreaker::BreakResult NGLineBreaker::BreakText( DCHECK_EQ(item_result->end_offset, item.EndOffset()); item_result->can_break_after = break_iterator_.IsBreakable(item_result->end_offset); + if (!item_result->can_break_after && item.Type() == NGInlineItem::kText && + IsAtomicInlineAfterNoBreakSpace(*item_result)) + item_result->can_break_after = true; trailing_whitespace_ = WhitespaceState::kUnknown; } + + // This result is not breakable any further if overflow. This information is + // useful to optimize |HandleOverflow()|. + item_result->may_break_inside = !result.is_overflow; + + // TODO(crbug.com/1003742): We should use |result.is_overflow| here. For now, + // use |inline_size| because some tests rely on this behavior. return inline_size <= available_width ? kSuccess : kOverflow; } @@ -1179,30 +1198,13 @@ void NGLineBreaker::HandleBidiControlItem(const NGInlineItem& item, MoveToNextOf(item); } -bool NGLineBreaker::HandleAtomicInline( +void NGLineBreaker::HandleAtomicInline( const NGInlineItem& item, LayoutUnit percentage_resolution_block_size_for_min_max, NGLineInfo* line_info) { DCHECK_EQ(item.Type(), NGInlineItem::kAtomicInline); DCHECK(item.Style()); const ComputedStyle& style = *item.Style(); - const NGInlineItemResults& item_results = line_info->Results(); - - // If the sticky images quirk is enabled, and this is an image that - // follows text that doesn't end with something breakable, we cannot break - // between the two items. - bool is_sticky_image = sticky_images_quirk_ && IsImage(item); - if (UNLIKELY(is_sticky_image)) { - PreventBreakBeforeStickyImage(trailing_whitespace_, Text(), line_info); - } - - // Atomic inline is handled as if it is trailable, because it can prevent - // break-before. Check if the line should break before this item, after the - // last item's |can_break_after| is finalized for the quirk above. - if (state_ == LineBreakState::kTrailing && CanBreakAfterLast(item_results)) { - line_info->SetIsLastLine(false); - return false; - } NGInlineItemResult* item_result = AddItem(item, line_info); item_result->should_create_line_box = true; @@ -1251,21 +1253,11 @@ bool NGLineBreaker::HandleAtomicInline( trailing_whitespace_ = WhitespaceState::kNone; position_ += item_result->inline_size; ComputeCanBreakAfter(item_result, auto_wrap_, break_iterator_); + if (!item_result->can_break_after && auto_wrap_ && + IsAtomicInlineBeforeNoBreakSpace(*item_result)) + item_result->can_break_after = true; - if (UNLIKELY(is_sticky_image)) { - const auto& items = Items(); - if (item_index_ + 1 < items.size()) { - DCHECK_EQ(&item, &items[item_index_]); - const auto& next_item = items[item_index_ + 1]; - // This is an image, and we don't want to break after it, unless what - // comes after provides a break opportunity. Look ahead. We only want to - // break if the next item is an atomic inline that's not an image. - if (next_item.Type() != NGInlineItem::kAtomicInline || IsImage(next_item)) - item_result->can_break_after = false; - } - } MoveToNextOf(item); - return true; } // Performs layout and positions a float. @@ -1306,9 +1298,6 @@ void NGLineBreaker::HandleFloat(const NGInlineItem& item, return; } - if (ignore_floats_) - return; - // Make sure we populate the positioned_float inside the |item_result|. if (item_index_ <= handled_leading_floats_index_ && !leading_floats_.IsEmpty()) { @@ -1372,7 +1361,7 @@ void NGLineBreaker::HandleFloat(const NGInlineItem& item, NGLayoutOpportunity opportunity = exclusion_space_->FindLayoutOpportunity( {constraint_space_.BfcOffset().line_offset, bfc_block_offset}, - constraint_space_.AvailableSize().inline_size, LogicalSize()); + constraint_space_.AvailableSize().inline_size); DCHECK_EQ(bfc_block_offset, opportunity.rect.BlockStartOffset()); @@ -1759,6 +1748,23 @@ const ComputedStyle& NGLineBreaker::ComputeCurrentStyle( } void NGLineBreaker::SetCurrentStyle(const ComputedStyle& style) { + if (&style == current_style_.get()) { +#if DCHECK_IS_ON() + // Check that cache fields are already setup correctly. + DCHECK_EQ(auto_wrap_, style.AutoWrap()); + if (auto_wrap_) { + DCHECK_EQ(enable_soft_hyphen_, style.GetHyphens() != Hyphens::kNone); + DCHECK_EQ(break_iterator_.Locale(), style.LocaleForLineBreakIterator()); + } + ShapeResultSpacing<String> spacing(spacing_.Text()); + spacing.SetSpacing(style.GetFontDescription()); + DCHECK_EQ(spacing.LetterSpacing(), spacing_.LetterSpacing()); + DCHECK_EQ(spacing.WordSpacing(), spacing_.WordSpacing()); +#endif + return; + } + current_style_ = &style; + auto_wrap_ = style.AutoWrap(); if (auto_wrap_) { @@ -1794,17 +1800,10 @@ void NGLineBreaker::SetCurrentStyle(const ComputedStyle& style) { if (style.WhiteSpace() == EWhiteSpace::kBreakSpaces) break_iterator_.SetBreakSpace(BreakSpaceType::kAfterEverySpace); - } - - // The above calls are cheap & necessary. But the following are expensive - // and do not need to be reset every time if the style doesn't change, - // so avoid them if possible. - if (&style == current_style_.get()) - return; - current_style_ = &style; - if (auto_wrap_) break_iterator_.SetLocale(style.LocaleForLineBreakIterator()); + } + spacing_.SetSpacing(style.GetFontDescription()); } 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 a90313b2f08..fc24745e176 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 @@ -93,9 +93,11 @@ class CORE_EXPORT NGLineBreaker { } private: - const String& Text() const { return items_data_.text_content; } + const String& Text() const { return text_content_; } const Vector<NGInlineItem>& Items() const { return items_data_.items; } + String TextContentForLineBreak() const; + NGInlineItemResult* AddItem(const NGInlineItem&, unsigned end_offset, NGLineInfo*); @@ -159,10 +161,14 @@ class CORE_EXPORT NGLineBreaker { void HandleControlItem(const NGInlineItem&, NGLineInfo*); void HandleBidiControlItem(const NGInlineItem&, NGLineInfo*); - bool HandleAtomicInline( + void HandleAtomicInline( const NGInlineItem&, LayoutUnit percentage_resolution_block_size_for_min_max, NGLineInfo*); + bool IsAtomicInlineAfterNoBreakSpace( + const NGInlineItemResult& item_result) const; + bool IsAtomicInlineBeforeNoBreakSpace( + const NGInlineItemResult& item_result) const; void HandleFloat(const NGInlineItem&, Vector<LayoutObject*>* out_floats_for_min_max, NGLineInfo*); @@ -208,6 +214,8 @@ class CORE_EXPORT NGLineBreaker { NGInlineNode node_; + NGLineBreakerMode mode_; + // True if this line is the "first formatted line". // https://www.w3.org/TR/CSS22/selector.html#first-formatted-line bool is_first_formatted_line_ = false; @@ -240,15 +248,13 @@ class CORE_EXPORT NGLineBreaker { // the next line. bool is_after_forced_break_ = false; - bool ignore_floats_ = false; - - // Set in quirks mode when we're not supposed to break inside table cells - // between images, and between text and images. - bool sticky_images_quirk_ = false; - const NGInlineItemsData& items_data_; - NGLineBreakerMode mode_; + // The text content of this node. This is same as |items_data_.text_content| + // except when sticky images quirk is needed. See + // |NGInlineNode::TextContentForContentSize|. + String text_content_; + const NGConstraintSpace& constraint_space_; NGExclusionSpace* exclusion_space_; scoped_refptr<const NGInlineBreakToken> break_token_; 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 df93ec3768d..34eba2935cd 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 @@ -34,12 +34,11 @@ class NGLineBreakerTest : public NGLayoutTest { node.PrepareLayoutIfNeeded(); - NGConstraintSpace space = - NGConstraintSpaceBuilder(WritingMode::kHorizontalTb, - WritingMode::kHorizontalTb, - /* is_new_fc */ false) - .SetAvailableSize({available_width, kIndefiniteSize}) - .ToConstraintSpace(); + NGConstraintSpaceBuilder builder(WritingMode::kHorizontalTb, + WritingMode::kHorizontalTb, + /* is_new_fc */ false); + builder.SetAvailableSize({available_width, kIndefiniteSize}); + NGConstraintSpace space = builder.ToConstraintSpace(); scoped_refptr<NGInlineBreakToken> break_token; @@ -307,6 +306,26 @@ TEST_F(NGLineBreakerTest, WrapLastWord) { EXPECT_EQ("AAA BB CC", ToString(lines[1], node)); } +TEST_F(NGLineBreakerTest, WrapLetterSpacing) { + NGInlineNode node = CreateInlineNode(R"HTML( + <!DOCTYPE html> + <style> + #container { + font: 10px/1 Times; + letter-spacing: 10px; + width: 0px; + } + </style> + <div id=container>Star Wars</div> + )HTML"); + + Vector<NGInlineItemResults> lines; + lines = BreakLines(node, LayoutUnit(100)); + EXPECT_EQ(2u, lines.size()); + EXPECT_EQ("Star", ToString(lines[0], node)); + EXPECT_EQ("Wars", ToString(lines[1], node)); +} + TEST_F(NGLineBreakerTest, BoundaryInWord) { LoadAhem(); NGInlineNode node = CreateInlineNode(R"HTML( diff --git a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_truncator.cc b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_truncator.cc index 836de6ae3db..ddadc0953aa 100644 --- a/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_truncator.cc +++ b/chromium/third_party/blink/renderer/core/layout/ng/inline/ng_line_truncator.cc @@ -97,7 +97,9 @@ LayoutUnit NGLineTruncator::TruncateLine( // Create the ellipsis, associating it with the ellipsized child. LayoutObject* ellipsized_layout_object = ellpisized_child->PhysicalFragment()->GetMutableLayoutObject(); - DCHECK(ellipsized_layout_object && ellipsized_layout_object->IsInline()); + DCHECK(ellipsized_layout_object && ellipsized_layout_object->IsInline() && + (ellipsized_layout_object->IsText() || + ellipsized_layout_object->IsAtomicInlineLevel())); NGTextFragmentBuilder builder(line_style_->GetWritingMode()); builder.SetText(ellipsized_layout_object, ellipsis_text, ellipsis_style, true /* is_ellipsis_style */, @@ -171,6 +173,12 @@ bool NGLineTruncator::EllipsizeChild( if (!child->HasInFlowFragment()) return false; + // Inline boxes should not be ellipsized. Usually they will be created in the + // later phase, but empty inline box are already created. + if (child->layout_result && + child->layout_result->PhysicalFragment().IsInlineBox()) + return false; + // Can't place ellipsis if this child is completely outside of the box. LayoutUnit child_inline_offset = IsLtr(line_direction_) 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 7a453e31e5d..7fc614ba21c 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 @@ -57,9 +57,9 @@ class CORE_EXPORT NGPhysicalLineBoxFragment final NGLineHeightMetrics BaselineMetrics(FontBaseline) const; // Scrollable overflow. including contents, in the local coordinate. - // ScrollableOverflow is not precomputed/cached because it cannot be computed - // when LineBox is generated because it needs container dimensions to - // resolve relative position of its children. + // |ScrollableOverflow| is not precomputed/cached because it cannot be + // computed when LineBox is generated because it needs container dimensions + // to resolve relative position of its children. PhysicalRect ScrollableOverflow(const LayoutObject* container, const ComputedStyle* container_style, PhysicalSize container_physical_size) const; 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 b4dc039579c..10cae6bffed 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 @@ -11,6 +11,7 @@ #include "third_party/blink/renderer/core/layout/line/line_orientation_utils.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_inline_item.h" #include "third_party/blink/renderer/core/layout/ng/inline/ng_text_fragment_builder.h" +#include "third_party/blink/renderer/core/layout/ng/ng_ink_overflow.h" #include "third_party/blink/renderer/core/page/page.h" #include "third_party/blink/renderer/core/style/computed_style.h" #include "third_party/blink/renderer/platform/fonts/shaping/shape_result_view.h" @@ -20,9 +21,8 @@ namespace blink { namespace { struct SameSizeAsNGPhysicalTextFragment : NGPhysicalFragment { - void* pointers[2]; + void* pointers[3]; unsigned offsets[2]; - PhysicalRect rect; }; static_assert(sizeof(NGPhysicalTextFragment) == @@ -84,23 +84,6 @@ NGPhysicalTextFragment::NGPhysicalTextFragment(NGTextFragmentBuilder* builder) ink_overflow_computed_ = false; } -// Convert logical cooridnate to local physical coordinate. -PhysicalRect NGPhysicalTextFragment::ConvertToLocal( - const LayoutRect& logical_rect) const { - switch (LineOrientation()) { - case NGLineOrientation::kHorizontal: - return PhysicalRect(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 PhysicalRect(logical_rect); -} - // Compute the inline position from text offset, in logical coordinate relative // to this fragment. LayoutUnit NGPhysicalTextFragment::InlinePositionForOffset( @@ -175,73 +158,20 @@ PhysicalRect NGPhysicalTextFragment::LocalRect(unsigned start_offset, PhysicalRect NGPhysicalTextFragment::SelfInkOverflow() const { if (!ink_overflow_computed_) ComputeSelfInkOverflow(); - return self_ink_overflow_; -} - -void NGPhysicalTextFragment::ClearSelfInkOverflow() const { - self_ink_overflow_ = LocalRect(); + if (ink_overflow_) + return ink_overflow_->self_ink_overflow; + return LocalRect(); } void NGPhysicalTextFragment::ComputeSelfInkOverflow() const { ink_overflow_computed_ = true; if (UNLIKELY(!shape_result_)) { - ClearSelfInkOverflow(); + ink_overflow_ = nullptr; return; } - // Glyph bounds is in logical coordinate, origin at the alphabetic baseline. - FloatRect text_ink_bounds = Style().GetFont().TextInkBounds(PaintInfo()); - LayoutRect ink_overflow = EnclosingLayoutRect(text_ink_bounds); - - // Make the origin at the logical top of this fragment. - const ComputedStyle& style = Style(); - const Font& font = style.GetFont(); - if (const SimpleFontData* font_data = font.PrimaryFont()) { - ink_overflow.SetY( - ink_overflow.Y() + - font_data->GetFontMetrics().FixedAscent(kAlphabeticBaseline)); - } - - if (float stroke_width = style.TextStrokeWidth()) { - ink_overflow.Inflate(LayoutUnit::FromFloatCeil(stroke_width / 2.0f)); - } - - if (style.GetTextEmphasisMark() != TextEmphasisMark::kNone) { - LayoutUnit emphasis_mark_height = - LayoutUnit(font.EmphasisMarkHeight(style.TextEmphasisMarkString())); - DCHECK_GT(emphasis_mark_height, LayoutUnit()); - if (style.GetTextEmphasisLineLogicalSide() == LineLogicalSide::kOver) { - ink_overflow.ShiftYEdgeTo( - std::min(ink_overflow.Y(), -emphasis_mark_height)); - } else { - LayoutUnit logical_height = - style.IsHorizontalWritingMode() ? Size().height : Size().width; - ink_overflow.ShiftMaxYEdgeTo( - std::max(ink_overflow.MaxY(), logical_height + emphasis_mark_height)); - } - } - - if (ShadowList* text_shadow = style.TextShadow()) { - LayoutRectOutsets text_shadow_logical_outsets = - LineOrientationLayoutRectOutsets( - LayoutRectOutsets(text_shadow->RectOutsetsIncludingOriginal()), - style.GetWritingMode()); - text_shadow_logical_outsets.ClampNegativeToZero(); - ink_overflow.Expand(text_shadow_logical_outsets); - } - - // Uniting the frame rect ensures that non-ink spaces such side bearings, or - // even space characters, are included in the visual rect for decorations. - PhysicalRect local_ink_overflow = ConvertToLocal(ink_overflow); - PhysicalRect local_rect = LocalRect(); - if (local_rect.Contains(local_ink_overflow)) { - self_ink_overflow_ = local_rect; - return; - } - local_ink_overflow.Unite(local_rect); - local_ink_overflow.ExpandEdgesToPixelBoundaries(); - self_ink_overflow_ = local_ink_overflow; + ink_overflow_ = NGInkOverflow::TextInkOverflow(PaintInfo(), Style(), Size()); } scoped_refptr<const NGPhysicalTextFragment> 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 a0fc73c0160..e4b7ba1ca08 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 @@ -7,6 +7,7 @@ #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_ink_overflow.h" #include "third_party/blink/renderer/core/layout/ng/ng_physical_fragment.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" @@ -17,9 +18,9 @@ namespace blink { -struct PhysicalRect; class NGTextFragmentBuilder; class NGPhysicalTextFragment; +struct PhysicalRect; enum class AdjustMidCluster; @@ -149,10 +150,7 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { LayoutUnit (*round)(float), AdjustMidCluster) const; - PhysicalRect ConvertToLocal(const LayoutRect&) const; - void ComputeSelfInkOverflow() const; - void ClearSelfInkOverflow() const; // The text of NGInlineNode; i.e., of a parent block. The text for this // fragment is a substring(start_offset_, end_offset_) of this string. @@ -166,7 +164,7 @@ class CORE_EXPORT NGPhysicalTextFragment final : public NGPhysicalFragment { // Fragments are immutable but allow certain expensive data, specifically ink // overflow, to be cached as long as it is guaranteed to always recompute to // the same value. - mutable PhysicalRect self_ink_overflow_; + mutable std::unique_ptr<NGInkOverflow> ink_overflow_; friend class NGTextFragmentBuilder; }; |