[LayoutNG] Improve LayoutText::SetTextWithOffset() by reducing text shaping
This patch changes |LayoutText::SetTextWithOffset()| to reduce text shaping by reusing |ShapeResult| of before and after replaced range for improving performance, 10% to 15% faster but 3 times slower than legacy layout. (Profile data: http://bit.ly/33B9rfg) From this patch, |SetTextWithOffset()| does: 1. Get offset mapping to map DOM offsets to text content offsets for replaced range. 2. Collect inlines into |text_content| and |NGInlineItem| to get newly inserted text. 3. Copy before and after |ShapeResult| from current data to |NGInlineItem| list in step 2. 4. Call |NGInlineNode::ShapeText()| with |NGInlineItem| list to reuse |ShapeResult|. This patch gets rid of NGInlineNodeTest.InvalidateSetTextWithOffset because new test case replaces this test and this test call |SetTextWithOffset()| with wrong parameter. It shoulbe |SetTextWithOffset("baftere", 1, 4)| as result of |"before".replaceData(1, 4, "after")|. Note: This is reland of http://crrev.com/c/1729330 TBR=yosin@chromium.org Bug: 707656, 998507,998683, 999005 Change-Id: I016180de37b7afcb71998658ad66c001482029fd Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1775647 Reviewed-by: Yoshifumi Inoue <yosin@chromium.org> Commit-Queue: Yoshifumi Inoue <yosin@chromium.org> Auto-Submit: Yoshifumi Inoue <yosin@chromium.org> Cr-Commit-Position: refs/heads/master@{#691893}
This commit is contained in:

committed by
Commit Bot

parent
31fc62a978
commit
e388dd67d6
third_party/blink
40
third_party/blink/perf_tests/layout/editing_append.html
vendored
Normal file
40
third_party/blink/perf_tests/layout/editing_append.html
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<body>
|
||||
<script src="../resources/runner.js"></script>
|
||||
<script src="resources/line-layout-perf-test.js"></script>
|
||||
<style>
|
||||
#container {
|
||||
width: 120ch;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
<pre id="container">A</pre>
|
||||
<script>
|
||||
const kNumberOfWords = 100;
|
||||
const kSampleText = (() => {
|
||||
const words = [];
|
||||
for (let i = 1; i <= kNumberOfWords; ++i) {
|
||||
words.push(TextGenerator.createWord(i % 12 + 3));
|
||||
if (i % 10 === 0)
|
||||
words.push('\n');
|
||||
}
|
||||
return words.join(' ');
|
||||
})().repeat(1);
|
||||
|
||||
const kCount = 10;
|
||||
|
||||
const container = document.getElementById('container');
|
||||
const editable = container.firstChild;
|
||||
|
||||
PerfTestRunner.measureRunsPerSecond({
|
||||
run: function() {
|
||||
for (let i = 0; i < kCount; ++i) {
|
||||
editable.nodeValue = kSampleText;
|
||||
PerfTestRunner.forceLayout();
|
||||
editable.appendData('xyz');
|
||||
PerfTestRunner.forceLayout();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
38
third_party/blink/perf_tests/layout/editing_append_single_line.html
vendored
Normal file
38
third_party/blink/perf_tests/layout/editing_append_single_line.html
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<body>
|
||||
<script src="../resources/runner.js"></script>
|
||||
<script src="resources/line-layout-perf-test.js"></script>
|
||||
<style>
|
||||
#container {
|
||||
width: 120ch;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
<pre id="container">A</pre>
|
||||
<script>
|
||||
const kNumberOfWords = 10;
|
||||
const kSampleText = (() => {
|
||||
const words = [];
|
||||
for (let i = 1; i <= kNumberOfWords; ++i) {
|
||||
words.push(TextGenerator.createWord(i % 12 + 3));
|
||||
}
|
||||
return words.join(' ');
|
||||
})().repeat(1);
|
||||
|
||||
const kCount = 10;
|
||||
|
||||
const container = document.getElementById('container');
|
||||
const editable = container.firstChild;
|
||||
|
||||
PerfTestRunner.measureRunsPerSecond({
|
||||
run: function() {
|
||||
for (let i = 0; i < kCount; ++i) {
|
||||
editable.nodeValue = kSampleText;
|
||||
PerfTestRunner.forceLayout();
|
||||
editable.appendData('XYZ');
|
||||
PerfTestRunner.forceLayout();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
40
third_party/blink/perf_tests/layout/editing_delete.html
vendored
Normal file
40
third_party/blink/perf_tests/layout/editing_delete.html
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<body>
|
||||
<script src="../resources/runner.js"></script>
|
||||
<script src="resources/line-layout-perf-test.js"></script>
|
||||
<style>
|
||||
#container {
|
||||
width: 120ch;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
<pre id="container">A</pre>
|
||||
<script>
|
||||
const kNumberOfWords = 100;
|
||||
const kSampleText = (() => {
|
||||
const words = [];
|
||||
for (let i = 1; i <= kNumberOfWords; ++i) {
|
||||
words.push(TextGenerator.createWord(i % 12 + 3));
|
||||
if (i % 10 === 0)
|
||||
words.push('\n');
|
||||
}
|
||||
return words.join(' ');
|
||||
})().repeat(1);
|
||||
|
||||
const kCount = 10;
|
||||
|
||||
const container = document.getElementById('container');
|
||||
const editable = container.firstChild;
|
||||
|
||||
PerfTestRunner.measureRunsPerSecond({
|
||||
run: function() {
|
||||
for (let i = 0; i < kCount; ++i) {
|
||||
editable.nodeValue = kSampleText;
|
||||
PerfTestRunner.forceLayout();
|
||||
editable.deleteData(10, 20);
|
||||
PerfTestRunner.forceLayout();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
40
third_party/blink/perf_tests/layout/editing_insert.html
vendored
Normal file
40
third_party/blink/perf_tests/layout/editing_insert.html
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<body>
|
||||
<script src="../resources/runner.js"></script>
|
||||
<script src="resources/line-layout-perf-test.js"></script>
|
||||
<style>
|
||||
#container {
|
||||
width: 120ch;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
<pre id="container">A</pre>
|
||||
<script>
|
||||
const kNumberOfWords = 100;
|
||||
const kSampleText = (() => {
|
||||
const words = [];
|
||||
for (let i = 1; i <= kNumberOfWords; ++i) {
|
||||
words.push(TextGenerator.createWord(i % 12 + 3));
|
||||
if (i % 10 === 0)
|
||||
words.push('\n');
|
||||
}
|
||||
return words.join(' ');
|
||||
})().repeat(1);
|
||||
|
||||
const kCount = 10;
|
||||
|
||||
const container = document.getElementById('container');
|
||||
const editable = container.firstChild;
|
||||
|
||||
PerfTestRunner.measureRunsPerSecond({
|
||||
run: function() {
|
||||
for (let i = 0; i < kCount; ++i) {
|
||||
editable.nodeValue = kSampleText;
|
||||
PerfTestRunner.forceLayout();
|
||||
editable.insertData(10, 'xyz');
|
||||
PerfTestRunner.forceLayout();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
40
third_party/blink/perf_tests/layout/editing_prepend.html
vendored
Normal file
40
third_party/blink/perf_tests/layout/editing_prepend.html
vendored
Normal file
@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<body>
|
||||
<script src="../resources/runner.js"></script>
|
||||
<script src="resources/line-layout-perf-test.js"></script>
|
||||
<style>
|
||||
#container {
|
||||
width: 120ch;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
</style>
|
||||
<pre id="container">A</pre>
|
||||
<script>
|
||||
const kNumberOfWords = 100;
|
||||
const kSampleText = (() => {
|
||||
const words = [];
|
||||
for (let i = 1; i <= kNumberOfWords; ++i) {
|
||||
words.push(TextGenerator.createWord(i % 12 + 3));
|
||||
if (i % 10 === 0)
|
||||
words.push('\n');
|
||||
}
|
||||
return words.join(' ');
|
||||
})().repeat(1);
|
||||
|
||||
const kCount = 10;
|
||||
|
||||
const container = document.getElementById('container');
|
||||
const editable = container.firstChild;
|
||||
|
||||
PerfTestRunner.measureRunsPerSecond({
|
||||
run: function() {
|
||||
for (let i = 0; i < kCount; ++i) {
|
||||
editable.nodeValue = kSampleText;
|
||||
PerfTestRunner.forceLayout();
|
||||
editable.insertData(0, 'xyz');
|
||||
PerfTestRunner.forceLayout();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
1
third_party/blink/renderer/core/BUILD.gn
vendored
1
third_party/blink/renderer/core/BUILD.gn
vendored
@ -1359,6 +1359,7 @@ jumbo_source_set("unit_tests") {
|
||||
"layout/ng/exclusions/ng_exclusion_space_test.cc",
|
||||
"layout/ng/geometry/ng_box_strut_test.cc",
|
||||
"layout/ng/geometry/ng_static_position_test.cc",
|
||||
"layout/ng/inline/layout_ng_text_test.cc",
|
||||
"layout/ng/inline/ng_baseline_test.cc",
|
||||
"layout/ng/inline/ng_caret_position_test.cc",
|
||||
"layout/ng/inline/ng_fragment_item_test.cc",
|
||||
|
@ -1323,6 +1323,7 @@ class CORE_EXPORT LayoutObject : public ImageResourceObserver,
|
||||
void SetNeedsCollectInlines();
|
||||
void SetChildNeedsCollectInlines();
|
||||
void ClearNeedsCollectInlines() { SetNeedsCollectInlines(false); }
|
||||
void SetNeedsCollectInlines(bool b) { bitfields_.SetNeedsCollectInlines(b); }
|
||||
|
||||
void MarkContainerChainForLayout(bool schedule_relayout = true,
|
||||
SubtreeLayoutScope* = nullptr);
|
||||
@ -3242,7 +3243,6 @@ class CORE_EXPORT LayoutObject : public ImageResourceObserver,
|
||||
void SetNeedsSimplifiedNormalFlowLayout(bool b) {
|
||||
bitfields_.SetNeedsSimplifiedNormalFlowLayout(b);
|
||||
}
|
||||
void SetNeedsCollectInlines(bool b) { bitfields_.SetNeedsCollectInlines(b); }
|
||||
|
||||
private:
|
||||
friend class LineLayoutItem;
|
||||
|
@ -1662,6 +1662,16 @@ void LayoutText::SetTextWithOffset(scoped_refptr<StringImpl> text,
|
||||
}
|
||||
}
|
||||
|
||||
if (NGInlineNode::SetTextWithOffset(this, text, offset, len)) {
|
||||
DCHECK(!NeedsCollectInlines());
|
||||
// Prevent |TextDidChange()| to propagate |NeedsCollectInlines|
|
||||
SetNeedsCollectInlines(true);
|
||||
TextDidChange();
|
||||
valid_ng_items_ = true;
|
||||
ClearNeedsCollectInlines();
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned old_len = TextLength();
|
||||
unsigned new_len = text->length();
|
||||
int delta = new_len - old_len;
|
||||
|
@ -192,6 +192,7 @@ class CORE_EXPORT LayoutText : public LayoutObject {
|
||||
void SetTextWithOffset(scoped_refptr<StringImpl>,
|
||||
unsigned offset,
|
||||
unsigned len);
|
||||
void SetTextInternal(scoped_refptr<StringImpl>);
|
||||
|
||||
virtual void TransformText();
|
||||
|
||||
@ -322,11 +323,12 @@ class CORE_EXPORT LayoutText : public LayoutObject {
|
||||
void SetHasBidiControlInlineItems() { has_bidi_control_items_ = true; }
|
||||
void ClearHasBidiControlInlineItems() { has_bidi_control_items_ = false; }
|
||||
|
||||
protected:
|
||||
virtual const base::span<NGInlineItem>* GetNGInlineItems() const {
|
||||
return nullptr;
|
||||
}
|
||||
virtual base::span<NGInlineItem>* GetNGInlineItems() { return nullptr; }
|
||||
|
||||
protected:
|
||||
void WillBeDestroyed() override;
|
||||
|
||||
void StyleWillChange(StyleDifference, const ComputedStyle&) final {}
|
||||
@ -334,7 +336,6 @@ class CORE_EXPORT LayoutText : public LayoutObject {
|
||||
|
||||
void InLayoutNGInlineFormattingContextWillChange(bool) final;
|
||||
|
||||
void SetTextInternal(scoped_refptr<StringImpl>);
|
||||
virtual void TextDidChange();
|
||||
|
||||
virtual InlineTextBox* CreateTextBox(int start,
|
||||
|
258
third_party/blink/renderer/core/layout/ng/inline/layout_ng_text_test.cc
vendored
Normal file
258
third_party/blink/renderer/core/layout/ng/inline/layout_ng_text_test.cc
vendored
Normal file
@ -0,0 +1,258 @@
|
||||
// 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, 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
|
@ -250,6 +250,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 {
|
||||
|
@ -131,6 +131,114 @@ 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);
|
||||
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) {
|
||||
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())
|
||||
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.
|
||||
@ -366,7 +474,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());
|
||||
@ -384,6 +492,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();
|
||||
@ -625,14 +1015,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 ||
|
||||
@ -717,43 +1106,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);
|
||||
|
@ -78,6 +78,15 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode {
|
||||
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 +133,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*);
|
||||
|
||||
|
@ -945,15 +945,6 @@ TEST_F(NGInlineNodeTest, InvalidateSetText) {
|
||||
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);
|
||||
EXPECT_TRUE(layout_block_flow_->NeedsCollectInlines());
|
||||
}
|
||||
|
||||
TEST_F(NGInlineNodeTest, InvalidateAddAbsolute) {
|
||||
SetupHtml("t",
|
||||
"<style>span { position: absolute; }</style>"
|
||||
|
Reference in New Issue
Block a user