Commit 1d3f2f6a authored by Yoichi Osato's avatar Yoichi Osato Committed by Commit Bot

Traverse on flat tree to mark and invalidate kInside LayoutObject in LayoutSelection

We mark and invalidate LayoutObject by
- finding first/last LayoutObject by canonicalization and
- traverse on layout tree
However we can those operation at once by traversing flat tree.
For that, this CL changes SelectionState::kInside marking from layout tree to flat tree.
I will change kStart/kEnd marking.
Core part is L517-L549, which replaces old SetSelectionState and CollectInvalidationSet.

LayoutSelectionTest.cpp:
 It traverses LayoutTree using NextInPreorder() and confirm each SelectionState
 and ShouldInvalidateSelection bit.
 This CL introduces InsertLayoutObjectAndAncestorBlocks() from CollectInvalidationSet()
 to construct PaintInvalidationSet in SetShouldInvalidateSelection().

Test expectations update:
 Since we traverse on flat tree, we skip pseudo element, or list style marker.

Bug: 739062
Change-Id: I0717340fb7eef2c130d1281c2c9f5d8ff9b9b586
Reviewed-on: https://chromium-review.googlesource.com/593529
Commit-Queue: Yoichi Osato <yoichio@chromium.org>
Reviewed-by: default avatarYoshifumi Inoue <yosin@chromium.org>
Reviewed-by: default avatarWalter Korman <wkorman@chromium.org>
Cr-Commit-Position: refs/heads/master@{#491988}
parent e9c88ae1
...@@ -317,6 +317,7 @@ source_set("unit_tests") { ...@@ -317,6 +317,7 @@ source_set("unit_tests") {
"FrameSelectionTest.cpp", "FrameSelectionTest.cpp",
"GranularityStrategyTest.cpp", "GranularityStrategyTest.cpp",
"InputMethodControllerTest.cpp", "InputMethodControllerTest.cpp",
"LayoutSelectionTest.cpp",
"PositionIteratorTest.cpp", "PositionIteratorTest.cpp",
"PositionTest.cpp", "PositionTest.cpp",
"RelocatablePositionTest.cpp", "RelocatablePositionTest.cpp",
......
...@@ -202,22 +202,28 @@ struct PaintInvalidationSet { ...@@ -202,22 +202,28 @@ struct PaintInvalidationSet {
DISALLOW_COPY_AND_ASSIGN(PaintInvalidationSet); DISALLOW_COPY_AND_ASSIGN(PaintInvalidationSet);
}; };
static void InsertLayoutObjectAndAncestorBlocks(
PaintInvalidationSet* invalidation_set,
LayoutObject* layout_object) {
invalidation_set->layout_objects.insert(layout_object);
for (LayoutBlock* containing_block = layout_object->ContainingBlock();
containing_block && !containing_block->IsLayoutView();
containing_block = containing_block->ContainingBlock()) {
const auto& result =
invalidation_set->layout_blocks.insert(containing_block);
if (!result.is_new_entry)
break;
}
}
static PaintInvalidationSet CollectInvalidationSet( static PaintInvalidationSet CollectInvalidationSet(
const SelectionPaintRange& range) { const SelectionPaintRange& range) {
if (range.IsNull()) if (range.IsNull())
return PaintInvalidationSet(); return PaintInvalidationSet();
PaintInvalidationSet invalidation_set; PaintInvalidationSet invalidation_set;
for (LayoutObject* runner : range) { for (LayoutObject* runner : range)
invalidation_set.layout_objects.insert(runner); InsertLayoutObjectAndAncestorBlocks(&invalidation_set, runner);
for (LayoutBlock* containing_block = runner->ContainingBlock();
containing_block && !containing_block->IsLayoutView();
containing_block = containing_block->ContainingBlock()) {
auto result = invalidation_set.layout_blocks.insert(containing_block);
if (!result.is_new_entry)
break;
}
}
return invalidation_set; return invalidation_set;
} }
...@@ -291,27 +297,6 @@ class SelectionMarkingRange { ...@@ -291,27 +297,6 @@ class SelectionMarkingRange {
DISALLOW_COPY_AND_ASSIGN(SelectionMarkingRange); DISALLOW_COPY_AND_ASSIGN(SelectionMarkingRange);
}; };
// Update the selection status of all LayoutObjects in |range|.
static void SetSelectionState(const SelectionPaintRange& range) {
if (range.IsNull())
return;
if (range.StartLayoutObject() == range.EndLayoutObject()) {
range.StartLayoutObject()->SetSelectionStateIfNeeded(
SelectionState::kStartAndEnd);
} else {
range.StartLayoutObject()->SetSelectionStateIfNeeded(
SelectionState::kStart);
range.EndLayoutObject()->SetSelectionStateIfNeeded(SelectionState::kEnd);
}
for (LayoutObject* runner : range) {
if (runner != range.StartLayoutObject() &&
runner != range.EndLayoutObject() && runner->CanBeSelectionLeaf())
runner->SetSelectionStateIfNeeded(SelectionState::kInside);
}
}
// Set ShouldInvalidateSelection flag of LayoutObjects // Set ShouldInvalidateSelection flag of LayoutObjects
// comparing them in |new_range| and |old_range|. // comparing them in |new_range| and |old_range|.
static void SetShouldInvalidateSelection(const SelectionMarkingRange& new_range, static void SetShouldInvalidateSelection(const SelectionMarkingRange& new_range,
...@@ -405,8 +390,7 @@ static SelectionMarkingRange CalcSelectionRangeAndSetSelectionState( ...@@ -405,8 +390,7 @@ static SelectionMarkingRange CalcSelectionRangeAndSetSelectionState(
return {}; return {};
// Find first/last LayoutObject and its offset. // Find first/last LayoutObject and its offset.
// TODO(yoichio): This traverse and marking(L405-L427) should be on Flat tree // TODO(yoichio): Find LayoutObject w/o canonicalization.
// rather than Layout tree.
const PositionInFlatTree& start_pos = const PositionInFlatTree& start_pos =
FindFirstVisiblePosition(selection.StartPosition()); FindFirstVisiblePosition(selection.StartPosition());
const PositionInFlatTree& end_pos = const PositionInFlatTree& end_pos =
...@@ -417,21 +401,52 @@ static SelectionMarkingRange CalcSelectionRangeAndSetSelectionState( ...@@ -417,21 +401,52 @@ static SelectionMarkingRange CalcSelectionRangeAndSetSelectionState(
// <div>foo<div style="visibility:hidden">^bar|</div>baz</div>. // <div>foo<div style="visibility:hidden">^bar|</div>baz</div>.
if (start_pos >= end_pos) if (start_pos >= end_pos)
return {}; return {};
LayoutObject* start_layout_object = start_pos.AnchorNode()->GetLayoutObject();
LayoutObject* end_layout_object = end_pos.AnchorNode()->GetLayoutObject(); LayoutObject* const start_layout_object =
start_pos.AnchorNode()->GetLayoutObject();
LayoutObject* const end_layout_object =
end_pos.AnchorNode()->GetLayoutObject();
DCHECK(start_layout_object); DCHECK(start_layout_object);
DCHECK(end_layout_object); DCHECK(end_layout_object);
DCHECK(start_layout_object->View() == end_layout_object->View()); DCHECK(start_layout_object->View() == end_layout_object->View());
if (!start_layout_object || !end_layout_object) if (!start_layout_object || !end_layout_object)
return {}; return {};
SelectionPaintRange range = { // Marking and collect invalidation candidate LayoutObjects.
start_layout_object, start_pos.ComputeEditingOffset(), end_layout_object, PaintInvalidationSet invalidation_set;
end_pos.ComputeEditingOffset()}; for (const Node& node :
SetSelectionState(range); EphemeralRangeInFlatTree(start_pos, end_pos).Nodes()) {
LayoutObject* const layout_object = node.GetLayoutObject();
if (!layout_object || layout_object == start_layout_object ||
layout_object == end_layout_object ||
!layout_object->CanBeSelectionLeaf())
continue;
layout_object->SetSelectionStateIfNeeded(SelectionState::kInside);
InsertLayoutObjectAndAncestorBlocks(&invalidation_set, layout_object);
}
if (start_layout_object == end_layout_object) {
start_layout_object->SetSelectionStateIfNeeded(
SelectionState::kStartAndEnd);
InsertLayoutObjectAndAncestorBlocks(&invalidation_set, start_layout_object);
} else {
start_layout_object->SetSelectionStateIfNeeded(SelectionState::kStart);
InsertLayoutObjectAndAncestorBlocks(&invalidation_set, start_layout_object);
end_layout_object->SetSelectionStateIfNeeded(SelectionState::kEnd);
InsertLayoutObjectAndAncestorBlocks(&invalidation_set, end_layout_object);
}
// TODO(yoichio): If start == end, they should be kStartAndEnd.
// If not, start.SelectionState == kStart and vice versa.
DCHECK(start_layout_object->GetSelectionState() == SelectionState::kStart ||
start_layout_object->GetSelectionState() ==
SelectionState::kStartAndEnd);
DCHECK(end_layout_object->GetSelectionState() == SelectionState::kEnd ||
end_layout_object->GetSelectionState() ==
SelectionState::kStartAndEnd);
return {start_layout_object, start_pos.ComputeEditingOffset(), return {start_layout_object, start_pos.ComputeEditingOffset(),
end_layout_object, end_pos.ComputeEditingOffset(), end_layout_object, end_pos.ComputeEditingOffset(),
CollectInvalidationSet(range)}; std::move(invalidation_set)};
} }
void LayoutSelection::SetHasPendingSelection() { void LayoutSelection::SetHasPendingSelection() {
...@@ -503,4 +518,23 @@ DEFINE_TRACE(LayoutSelection) { ...@@ -503,4 +518,23 @@ DEFINE_TRACE(LayoutSelection) {
visitor->Trace(frame_selection_); visitor->Trace(frame_selection_);
} }
#ifndef NDEBUG
void PrintLayoutObjectForSelection(std::ostream& ostream,
LayoutObject* layout_object) {
if (!layout_object) {
ostream << "<null>";
return;
}
ostream << layout_object->GetNode()
<< ", state:" << layout_object->GetSelectionState()
<< (layout_object->ShouldInvalidateSelection() ? ", ShouldInvalidate"
: ", NotInvalidate");
}
void ShowLayoutObjectForSelection(LayoutObject* layout_object) {
std::stringstream stream;
PrintLayoutObjectForSelection(stream, layout_object);
LOG(INFO) << '\n' << stream.str();
}
#endif
} // namespace blink } // namespace blink
...@@ -119,6 +119,11 @@ class LayoutSelection final : public GarbageCollected<LayoutSelection> { ...@@ -119,6 +119,11 @@ class LayoutSelection final : public GarbageCollected<LayoutSelection> {
SelectionPaintRange paint_range_; SelectionPaintRange paint_range_;
}; };
#ifndef NDEBUG
void CORE_EXPORT PrintLayoutObjectForSelection(std::ostream&, LayoutObject*);
void ShowLayoutObjectForSelection(LayoutObject*);
#endif
} // namespace blink } // namespace blink
#endif #endif
// Copyright 2017 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 "core/editing/LayoutSelection.h"
#include "bindings/core/v8/V8BindingForCore.h"
#include "core/dom/ShadowRootInit.h"
#include "core/editing/EditingTestBase.h"
#include "core/editing/FrameSelection.h"
#include "core/layout/LayoutObject.h"
#include "core/layout/LayoutText.h"
#include "platform/wtf/Assertions.h"
namespace blink {
class LayoutSelectionTest : public EditingTestBase {
public:
// Test LayoutObject's type, SelectionState and ShouldInvalidateSelection
// flag.
bool TestNextLayoutObject(bool (LayoutObject::*predicate)(void) const,
SelectionState state) {
if (!current_)
current_ = GetDocument().body()->GetLayoutObject();
else
current_ = current_->NextInPreOrder();
if (!current_)
return false;
if (current_->GetSelectionState() != state)
return false;
if (current_->ShouldInvalidateSelection() !=
(state != SelectionState::kNone))
return false;
if (!(current_->*predicate)())
return false;
return true;
}
bool TestNextLayoutObject(const String& text, SelectionState state) {
// TODO(yoichio): Share code using Function(see FunctionalTest.cpp).
if (!current_)
current_ = GetDocument().body()->GetLayoutObject();
else
current_ = current_->NextInPreOrder();
if (!current_)
return false;
if (current_->GetSelectionState() != state)
return false;
if (current_->ShouldInvalidateSelection() !=
(state != SelectionState::kNone))
return false;
if (!current_->IsText())
return false;
if (text != ToLayoutText(current_)->GetText())
return false;
return true;
}
LayoutObject* Current() { return current_; }
#ifndef NDEBUG
void PrintLayoutTreeForDebug() {
std::stringstream stream;
for (LayoutObject* runner = GetDocument().body()->GetLayoutObject(); runner;
runner = runner->NextInPreOrder()) {
PrintLayoutObjectForSelection(stream, runner);
stream << '\n';
}
LOG(INFO) << '\n' << stream.str();
}
#endif
private:
LayoutObject* current_ = nullptr;
};
#define TEST_NEXT(predicate, state) \
EXPECT_TRUE(TestNextLayoutObject(predicate, state)) << Current()
#define TEST_NO_NEXT_LAYOUT_OBJECT() \
EXPECT_EQ(Current()->NextInPreOrder(), nullptr)
TEST_F(LayoutSelectionTest, TraverseLayoutObject) {
SetBodyContent("foo<br>bar");
Selection().SetSelection(SelectionInDOMTree::Builder()
.SelectAllChildren(*GetDocument().body())
.Build());
Selection().CommitAppearanceIfNeeded();
TEST_NEXT(&LayoutObject::IsLayoutBlock, SelectionState::kStartAndEnd);
TEST_NEXT("foo", SelectionState::kStart);
TEST_NEXT(&LayoutObject::IsBR, SelectionState::kInside);
TEST_NEXT("bar", SelectionState::kEnd);
TEST_NO_NEXT_LAYOUT_OBJECT();
}
TEST_F(LayoutSelectionTest, TraverseLayoutObjectTruncateVisibilityHidden) {
SetBodyContent(
"<span style='visibility:hidden;'>before</span>"
"foo"
"<span style='visibility:hidden;'>after</span>");
Selection().SetSelection(SelectionInDOMTree::Builder()
.SelectAllChildren(*GetDocument().body())
.Build());
Selection().CommitAppearanceIfNeeded();
TEST_NEXT(&LayoutObject::IsLayoutBlock, SelectionState::kStartAndEnd);
TEST_NEXT(&LayoutObject::IsLayoutInline, SelectionState::kNone);
TEST_NEXT("before", SelectionState::kNone);
TEST_NEXT("foo", SelectionState::kStartAndEnd);
TEST_NEXT(&LayoutObject::IsLayoutInline, SelectionState::kNone);
TEST_NEXT("after", SelectionState::kNone);
TEST_NO_NEXT_LAYOUT_OBJECT();
}
TEST_F(LayoutSelectionTest, TraverseLayoutObjectTruncateBR) {
SetBodyContent("<br><br>foo<br><br>");
Selection().SetSelection(SelectionInDOMTree::Builder()
.SelectAllChildren(*GetDocument().body())
.Build());
Selection().CommitAppearanceIfNeeded();
TEST_NEXT(&LayoutObject::IsLayoutBlock, SelectionState::kStartAndEnd);
TEST_NEXT(&LayoutObject::IsBR, SelectionState::kStart);
TEST_NEXT(&LayoutObject::IsBR, SelectionState::kInside);
TEST_NEXT("foo", SelectionState::kInside);
TEST_NEXT(&LayoutObject::IsBR, SelectionState::kEnd);
TEST_NEXT(&LayoutObject::IsBR, SelectionState::kNone);
TEST_NO_NEXT_LAYOUT_OBJECT();
}
TEST_F(LayoutSelectionTest, TraverseLayoutObjectListStyleImage) {
SetBodyContent(
"<style>ul {list-style-image:url(data:"
"image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=)}"
"</style>"
"<ul><li>foo<li>bar</ul>");
Selection().SetSelection(SelectionInDOMTree::Builder()
.SelectAllChildren(*GetDocument().body())
.Build());
Selection().CommitAppearanceIfNeeded();
TEST_NEXT(&LayoutObject::IsLayoutBlock, SelectionState::kStartAndEnd);
TEST_NEXT(&LayoutObject::IsLayoutBlockFlow, SelectionState::kStartAndEnd);
TEST_NEXT(&LayoutObject::IsListItem, SelectionState::kStart);
TEST_NEXT(&LayoutObject::IsListMarker, SelectionState::kNone);
TEST_NEXT("foo", SelectionState::kStart);
TEST_NEXT(&LayoutObject::IsListItem, SelectionState::kEnd);
TEST_NEXT(&LayoutObject::IsListMarker, SelectionState::kNone);
TEST_NEXT("bar", SelectionState::kEnd);
TEST_NO_NEXT_LAYOUT_OBJECT();
}
TEST_F(LayoutSelectionTest, TraverseLayoutObjectCrossingShadowBoundary) {
#ifndef NDEBUG
PrintLayoutTreeForDebug();
#endif
SetBodyContent(
"foo<div id='host'>"
"<span slot='s1'>bar1</span><span slot='s2'>bar2</span>"
"</div>");
// TODO(yoichio): Move NodeTest::AttachShadowTo to EditingTestBase and use
// it.
ShadowRootInit shadow_root_init;
shadow_root_init.setMode("open");
ShadowRoot* const shadow_root =
GetDocument().QuerySelector("div")->attachShadow(
ToScriptStateForMainWorld(GetDocument().GetFrame()), shadow_root_init,
ASSERT_NO_EXCEPTION);
shadow_root->setInnerHTML(
"Foo<slot name='s2'></slot><slot name='s1'></slot>");
Selection().SetSelection(
SelectionInDOMTree::Builder()
.SetBaseAndExtent(Position(GetDocument().body(), 0),
Position(GetDocument().QuerySelector("span"), 0))
.Build());
Selection().CommitAppearanceIfNeeded();
TEST_NEXT(&LayoutObject::IsLayoutBlock, SelectionState::kStartAndEnd);
TEST_NEXT(&LayoutObject::IsLayoutBlockFlow, SelectionState::kStart);
TEST_NEXT("foo", SelectionState::kStart);
TEST_NEXT(&LayoutObject::IsLayoutBlock, SelectionState::kEnd);
TEST_NEXT("Foo", SelectionState::kInside);
TEST_NEXT(&LayoutObject::IsLayoutInline, SelectionState::kNone);
TEST_NEXT("bar2", SelectionState::kEnd);
TEST_NEXT(&LayoutObject::IsLayoutInline, SelectionState::kNone);
TEST_NEXT("bar1", SelectionState::kNone);
TEST_NO_NEXT_LAYOUT_OBJECT();
}
} // namespace blink
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment