[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/exclusions/ng_exclusion_space_test.cc",
|
||||||
"layout/ng/geometry/ng_box_strut_test.cc",
|
"layout/ng/geometry/ng_box_strut_test.cc",
|
||||||
"layout/ng/geometry/ng_static_position_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_baseline_test.cc",
|
||||||
"layout/ng/inline/ng_caret_position_test.cc",
|
"layout/ng/inline/ng_caret_position_test.cc",
|
||||||
"layout/ng/inline/ng_fragment_item_test.cc",
|
"layout/ng/inline/ng_fragment_item_test.cc",
|
||||||
|
@@ -1323,6 +1323,7 @@ class CORE_EXPORT LayoutObject : public ImageResourceObserver,
|
|||||||
void SetNeedsCollectInlines();
|
void SetNeedsCollectInlines();
|
||||||
void SetChildNeedsCollectInlines();
|
void SetChildNeedsCollectInlines();
|
||||||
void ClearNeedsCollectInlines() { SetNeedsCollectInlines(false); }
|
void ClearNeedsCollectInlines() { SetNeedsCollectInlines(false); }
|
||||||
|
void SetNeedsCollectInlines(bool b) { bitfields_.SetNeedsCollectInlines(b); }
|
||||||
|
|
||||||
void MarkContainerChainForLayout(bool schedule_relayout = true,
|
void MarkContainerChainForLayout(bool schedule_relayout = true,
|
||||||
SubtreeLayoutScope* = nullptr);
|
SubtreeLayoutScope* = nullptr);
|
||||||
@@ -3242,7 +3243,6 @@ class CORE_EXPORT LayoutObject : public ImageResourceObserver,
|
|||||||
void SetNeedsSimplifiedNormalFlowLayout(bool b) {
|
void SetNeedsSimplifiedNormalFlowLayout(bool b) {
|
||||||
bitfields_.SetNeedsSimplifiedNormalFlowLayout(b);
|
bitfields_.SetNeedsSimplifiedNormalFlowLayout(b);
|
||||||
}
|
}
|
||||||
void SetNeedsCollectInlines(bool b) { bitfields_.SetNeedsCollectInlines(b); }
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
friend class LineLayoutItem;
|
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 old_len = TextLength();
|
||||||
unsigned new_len = text->length();
|
unsigned new_len = text->length();
|
||||||
int delta = new_len - old_len;
|
int delta = new_len - old_len;
|
||||||
|
@@ -192,6 +192,7 @@ class CORE_EXPORT LayoutText : public LayoutObject {
|
|||||||
void SetTextWithOffset(scoped_refptr<StringImpl>,
|
void SetTextWithOffset(scoped_refptr<StringImpl>,
|
||||||
unsigned offset,
|
unsigned offset,
|
||||||
unsigned len);
|
unsigned len);
|
||||||
|
void SetTextInternal(scoped_refptr<StringImpl>);
|
||||||
|
|
||||||
virtual void TransformText();
|
virtual void TransformText();
|
||||||
|
|
||||||
@@ -322,11 +323,12 @@ class CORE_EXPORT LayoutText : public LayoutObject {
|
|||||||
void SetHasBidiControlInlineItems() { has_bidi_control_items_ = true; }
|
void SetHasBidiControlInlineItems() { has_bidi_control_items_ = true; }
|
||||||
void ClearHasBidiControlInlineItems() { has_bidi_control_items_ = false; }
|
void ClearHasBidiControlInlineItems() { has_bidi_control_items_ = false; }
|
||||||
|
|
||||||
protected:
|
|
||||||
virtual const base::span<NGInlineItem>* GetNGInlineItems() const {
|
virtual const base::span<NGInlineItem>* GetNGInlineItems() const {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
virtual base::span<NGInlineItem>* GetNGInlineItems() { return nullptr; }
|
virtual base::span<NGInlineItem>* GetNGInlineItems() { return nullptr; }
|
||||||
|
|
||||||
|
protected:
|
||||||
void WillBeDestroyed() override;
|
void WillBeDestroyed() override;
|
||||||
|
|
||||||
void StyleWillChange(StyleDifference, const ComputedStyle&) final {}
|
void StyleWillChange(StyleDifference, const ComputedStyle&) final {}
|
||||||
@@ -334,7 +336,6 @@ class CORE_EXPORT LayoutText : public LayoutObject {
|
|||||||
|
|
||||||
void InLayoutNGInlineFormattingContextWillChange(bool) final;
|
void InLayoutNGInlineFormattingContextWillChange(bool) final;
|
||||||
|
|
||||||
void SetTextInternal(scoped_refptr<StringImpl>);
|
|
||||||
virtual void TextDidChange();
|
virtual void TextDidChange();
|
||||||
|
|
||||||
virtual InlineTextBox* CreateTextBox(int start,
|
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_symbol_marker_ : 1;
|
||||||
unsigned is_generated_for_line_break_ : 1;
|
unsigned is_generated_for_line_break_ : 1;
|
||||||
friend class NGInlineNode;
|
friend class NGInlineNode;
|
||||||
|
friend class NGInlineNodeDataEditor;
|
||||||
};
|
};
|
||||||
|
|
||||||
inline void NGInlineItem::AssertOffset(unsigned offset) const {
|
inline void NGInlineItem::AssertOffset(unsigned offset) const {
|
||||||
|
@@ -131,6 +131,114 @@ class ItemsBuilderForMarkLineBoxesDirty {
|
|||||||
bool has_floating_or_out_of_flow_positioned_ = false;
|
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:
|
// The function is templated to indicate the purpose of collected inlines:
|
||||||
// - With EmptyOffsetMappingBuilder: updating layout;
|
// - With EmptyOffsetMappingBuilder: updating layout;
|
||||||
// - With NGOffsetMappingBuilder: building offset mapping on clean layout.
|
// - With NGOffsetMappingBuilder: building offset mapping on clean layout.
|
||||||
@@ -366,7 +474,7 @@ void NGInlineNode::PrepareLayout(
|
|||||||
DCHECK(data);
|
DCHECK(data);
|
||||||
CollectInlines(data, previous_data.get(), dirty_lines);
|
CollectInlines(data, previous_data.get(), dirty_lines);
|
||||||
SegmentText(data);
|
SegmentText(data);
|
||||||
ShapeText(data, previous_data.get());
|
ShapeText(data, &previous_data->text_content);
|
||||||
ShapeTextForFirstLineIfNeeded(data);
|
ShapeTextForFirstLineIfNeeded(data);
|
||||||
AssociateItemsWithInlines(data);
|
AssociateItemsWithInlines(data);
|
||||||
DCHECK_EQ(data, MutableData());
|
DCHECK_EQ(data, MutableData());
|
||||||
@@ -384,6 +492,288 @@ void NGInlineNode::PrepareLayout(
|
|||||||
#endif
|
#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() {
|
const NGInlineNodeData& NGInlineNode::EnsureData() {
|
||||||
PrepareLayoutIfNeeded();
|
PrepareLayoutIfNeeded();
|
||||||
return Data();
|
return Data();
|
||||||
@@ -625,14 +1015,13 @@ void NGInlineNode::SegmentBidiRuns(NGInlineNodeData* data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void NGInlineNode::ShapeText(NGInlineItemsData* 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;
|
const String& text_content = data->text_content;
|
||||||
Vector<NGInlineItem>* items = &data->items;
|
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.
|
// Provide full context of the entire node to the shaper.
|
||||||
HarfBuzzShaper shaper(text_content);
|
ReusingTextShaper shaper(data, previous_items);
|
||||||
ShapeResultSpacing<String> spacing(text_content);
|
ShapeResultSpacing<String> spacing(text_content);
|
||||||
|
|
||||||
DCHECK(!data->segments ||
|
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.
|
// Results may only be reused if all items in the range remain valid.
|
||||||
bool has_valid_shape_results = true;
|
if (previous_text) {
|
||||||
for (unsigned item_index = index; item_index < end_index; item_index++) {
|
bool has_valid_shape_results = true;
|
||||||
if (NeedsShaping((*items)[item_index])) {
|
for (unsigned item_index = index; item_index < end_index; item_index++) {
|
||||||
has_valid_shape_results = false;
|
if (NeedsShaping((*items)[item_index])) {
|
||||||
break;
|
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.
|
// Shape each item with the full context of the entire node.
|
||||||
scoped_refptr<ShapeResult> shape_result;
|
scoped_refptr<ShapeResult> shape_result =
|
||||||
if (!data->segments) {
|
shaper.Shape(start_item, end_offset);
|
||||||
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());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (UNLIKELY(spacing.SetSpacing(font.GetFontDescription())))
|
if (UNLIKELY(spacing.SetSpacing(font.GetFontDescription())))
|
||||||
shape_result->ApplySpacing(spacing);
|
shape_result->ApplySpacing(spacing);
|
||||||
|
@@ -78,6 +78,15 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode {
|
|||||||
static void ClearAssociatedFragments(const NGPhysicalFragment& fragment,
|
static void ClearAssociatedFragments(const NGPhysicalFragment& fragment,
|
||||||
const NGBlockBreakToken* break_token);
|
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
|
// Returns the DOM to text content offset mapping of this block. If it is not
|
||||||
// computed before, compute and store it in NGInlineNodeData.
|
// computed before, compute and store it in NGInlineNodeData.
|
||||||
// This funciton must be called with clean layout.
|
// This funciton must be called with clean layout.
|
||||||
@@ -124,7 +133,8 @@ class CORE_EXPORT NGInlineNode : public NGLayoutInputNode {
|
|||||||
void SegmentFontOrientation(NGInlineNodeData*);
|
void SegmentFontOrientation(NGInlineNodeData*);
|
||||||
void SegmentBidiRuns(NGInlineNodeData*);
|
void SegmentBidiRuns(NGInlineNodeData*);
|
||||||
void ShapeText(NGInlineItemsData*,
|
void ShapeText(NGInlineItemsData*,
|
||||||
NGInlineItemsData* previous_data = nullptr);
|
const String* previous_text = nullptr,
|
||||||
|
const Vector<NGInlineItem>* previous_items = nullptr);
|
||||||
void ShapeTextForFirstLineIfNeeded(NGInlineNodeData*);
|
void ShapeTextForFirstLineIfNeeded(NGInlineNodeData*);
|
||||||
void AssociateItemsWithInlines(NGInlineNodeData*);
|
void AssociateItemsWithInlines(NGInlineNodeData*);
|
||||||
|
|
||||||
|
@@ -945,15 +945,6 @@ TEST_F(NGInlineNodeTest, InvalidateSetText) {
|
|||||||
EXPECT_TRUE(layout_block_flow_->NeedsCollectInlines());
|
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) {
|
TEST_F(NGInlineNodeTest, InvalidateAddAbsolute) {
|
||||||
SetupHtml("t",
|
SetupHtml("t",
|
||||||
"<style>span { position: absolute; }</style>"
|
"<style>span { position: absolute; }</style>"
|
||||||
|
Reference in New Issue
Block a user