Commit d4035bb9 authored by Vladimir Levin's avatar Vladimir Levin Committed by Commit Bot

SubtreeVisibility: Ensure selected elements remain unlocked.

This patch is similar to the focus patch ensuring that we remain
unlocked when any of the elements in our subtree is focused.

It's a bit more complicated since multiple nodes can be selected at once

Note the use of std::set here is because it is both ordered and only
keeps unique elements. The use of it is limited to the local function
which I think is fine.

R=chrishtr@chromium.org

Change-Id: I644c75bc145a0723f868fd7145840e2343837e8b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2122857Reviewed-by: default avatarChris Harrelson <chrishtr@chromium.org>
Commit-Queue: vmpstr <vmpstr@chromium.org>
Cr-Commit-Position: refs/heads/master@{#755024}
parent 82448bb5
......@@ -17,6 +17,8 @@
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/editing/frame_selection.h"
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/html_element_type_helpers.h"
#include "third_party/blink/renderer/core/inspector/inspector_trace_events.h"
......@@ -91,6 +93,7 @@ DisplayLockContext::DisplayLockContext(Element* element)
: element_(element), document_(&element_->GetDocument()) {
document_->AddDisplayLockContext(this);
DetermineIfSubtreeHasFocus();
DetermineIfSubtreeHasSelection();
}
void DisplayLockContext::SetRequestedState(ESubtreeVisibility state) {
......@@ -756,6 +759,7 @@ void DisplayLockContext::DidMoveToNewDocument(Document& old_document) {
}
DetermineIfSubtreeHasFocus();
DetermineIfSubtreeHasSelection();
}
void DisplayLockContext::WillStartLifecycleUpdate(const LocalFrameView& view) {
......@@ -793,6 +797,7 @@ void DisplayLockContext::ElementDisconnected() {
void DisplayLockContext::ElementConnected() {
UpdateActivationObservationIfNeeded();
DetermineIfSubtreeHasFocus();
DetermineIfSubtreeHasSelection();
}
void DisplayLockContext::ScheduleAnimation() {
......@@ -905,6 +910,39 @@ void DisplayLockContext::DetermineIfSubtreeHasFocus() {
subtree_has_focus);
}
void DisplayLockContext::NotifySubtreeGainedSelection() {
SetRenderAffectingState(RenderAffectingState::kSubtreeHasSelection, true);
}
void DisplayLockContext::NotifySubtreeLostSelection() {
SetRenderAffectingState(RenderAffectingState::kSubtreeHasSelection, false);
}
void DisplayLockContext::DetermineIfSubtreeHasSelection() {
if (!ConnectedToView() || !document_->GetFrame()) {
SetRenderAffectingState(RenderAffectingState::kSubtreeHasSelection, false);
return;
}
auto range = ToEphemeralRangeInFlatTree(document_->GetFrame()
->Selection()
.GetSelectionInDOMTree()
.ComputeRange());
bool subtree_has_selection = false;
for (auto& node : range.Nodes()) {
for (auto& ancestor : FlatTreeTraversal::InclusiveAncestorsOf(node)) {
if (&ancestor == element_.Get()) {
subtree_has_selection = true;
break;
}
}
if (subtree_has_selection)
break;
}
SetRenderAffectingState(RenderAffectingState::kSubtreeHasSelection,
subtree_has_selection);
}
void DisplayLockContext::SetRenderAffectingState(RenderAffectingState state,
bool new_flag) {
render_affecting_state_[static_cast<int>(state)] = new_flag;
......@@ -927,11 +965,15 @@ void DisplayLockContext::NotifyRenderAffectingStateChanged() {
// following is true:
// - We are not in 'auto' mode (meaning 'hidden') or
// - We are in 'auto' mode and nothing blocks locking: viewport is
// not intersecting, and subtree doesn't have focus.
bool should_be_locked = state(RenderAffectingState::kLockRequested) &&
(state_ != ESubtreeVisibility::kAuto ||
(!state(RenderAffectingState::kIntersectsViewport) &&
!state(RenderAffectingState::kSubtreeHasFocus)));
// not intersecting, subtree doesn't have focus, and subtree doesn't have
// selection.
bool should_be_locked =
state(RenderAffectingState::kLockRequested) &&
(state_ != ESubtreeVisibility::kAuto ||
(!state(RenderAffectingState::kIntersectsViewport) &&
!state(RenderAffectingState::kSubtreeHasFocus) &&
!state(RenderAffectingState::kSubtreeHasSelection)));
if (should_be_locked && !IsLocked())
Lock();
else if (!should_be_locked && IsLocked())
......
......@@ -191,6 +191,9 @@ class CORE_EXPORT DisplayLockContext final
void NotifySubtreeLostFocus();
void NotifySubtreeGainedFocus();
void NotifySubtreeLostSelection();
void NotifySubtreeGainedSelection();
void SetNeedsPrePaintSubtreeWalk(
bool needs_effective_allowed_touch_action_update) {
needs_effective_allowed_touch_action_update_ =
......@@ -308,6 +311,10 @@ class CORE_EXPORT DisplayLockContext final
// element to its root element.
void DetermineIfSubtreeHasFocus();
// Determines if the subtree has selection. This will walk from each of the
// selected notes up to its root looking for `element_`.
void DetermineIfSubtreeHasSelection();
WeakMember<Element> element_;
WeakMember<Document> document_;
ESubtreeVisibility state_ = ESubtreeVisibility::kVisible;
......@@ -363,6 +370,7 @@ class CORE_EXPORT DisplayLockContext final
kLockRequested,
kIntersectsViewport,
kSubtreeHasFocus,
kSubtreeHasSelection,
kNumRenderAffectingStates
};
void SetRenderAffectingState(RenderAffectingState state, bool flag);
......
......@@ -12,9 +12,12 @@
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/editing/editing_boundary.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/inspector/inspector_trace_events.h"
#include "third_party/blink/renderer/core/layout/layout_embedded_content.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include <set>
namespace blink {
namespace {
......@@ -57,6 +60,18 @@ bool UpdateStyleAndLayoutForRangeIfNeeded(const EphemeralRangeInFlatTree& range,
return !scoped_forced_update_list_.IsEmpty();
}
void PopulateAncestorContexts(Node* node,
std::set<DisplayLockContext*>* contexts) {
DCHECK(node);
for (Node& ancestor : FlatTreeTraversal::InclusiveAncestorsOf(*node)) {
auto* ancestor_element = DynamicTo<Element>(ancestor);
if (!ancestor_element)
continue;
if (auto* context = ancestor_element->GetDisplayLockContext())
contexts->insert(context);
}
}
} // namespace
bool DisplayLockUtilities::ActivateFindInPageMatchRangeIfNeeded(
......@@ -348,7 +363,8 @@ bool DisplayLockUtilities::IsInLockedSubtreeCrossingFrames(
}
void DisplayLockUtilities::ElementLostFocus(Element* element) {
if (!RuntimeEnabledFeatures::CSSSubtreeVisibilityEnabled())
if (!RuntimeEnabledFeatures::CSSSubtreeVisibilityEnabled() ||
(element && element->GetDocument().DisplayLockCount() == 0))
return;
for (; element; element = FlatTreeTraversal::ParentElement(*element)) {
auto* context = element->GetDisplayLockContext();
......@@ -357,8 +373,10 @@ void DisplayLockUtilities::ElementLostFocus(Element* element) {
}
}
void DisplayLockUtilities::ElementGainedFocus(Element* element) {
if (!RuntimeEnabledFeatures::CSSSubtreeVisibilityEnabled())
if (!RuntimeEnabledFeatures::CSSSubtreeVisibilityEnabled() ||
(element && element->GetDocument().DisplayLockCount() == 0))
return;
for (; element; element = FlatTreeTraversal::ParentElement(*element)) {
auto* context = element->GetDisplayLockContext();
if (context)
......@@ -366,4 +384,77 @@ void DisplayLockUtilities::ElementGainedFocus(Element* element) {
}
}
void DisplayLockUtilities::SelectionChanged(
const EphemeralRangeInFlatTree& old_selection,
const EphemeralRangeInFlatTree& new_selection) {
if (!RuntimeEnabledFeatures::CSSSubtreeVisibilityEnabled() ||
(!old_selection.IsNull() &&
old_selection.GetDocument().DisplayLockCount() == 0) ||
(!new_selection.IsNull() &&
new_selection.GetDocument().DisplayLockCount() == 0))
return;
TRACE_EVENT0("blink", "DisplayLockUtilities::SelectionChanged");
std::set<Node*> old_nodes;
for (Node& node : old_selection.Nodes())
old_nodes.insert(&node);
std::set<Node*> new_nodes;
for (Node& node : new_selection.Nodes())
new_nodes.insert(&node);
std::set<DisplayLockContext*> lost_selection_contexts;
std::set<DisplayLockContext*> gained_selection_contexts;
// Skip common nodes and extract contexts from nodes that lost selection and
// contexts from nodes that gained selection.
// This is similar to std::set_symmetric_difference except that we need to
// know which set the resulting item came from. In this version, we simply do
// the relevant operation on each of the items instead of storing the
// difference.
std::set<Node*>::iterator old_it = old_nodes.begin();
std::set<Node*>::iterator new_it = new_nodes.begin();
while (old_it != old_nodes.end() && new_it != new_nodes.end()) {
// Compare the addresses since that's how the nodes are ordered in the set.
if (*old_it < *new_it) {
PopulateAncestorContexts(*old_it++, &lost_selection_contexts);
} else if (*old_it > *new_it) {
PopulateAncestorContexts(*new_it++, &gained_selection_contexts);
} else {
++old_it;
++new_it;
}
}
while (old_it != old_nodes.end())
PopulateAncestorContexts(*old_it++, &lost_selection_contexts);
while (new_it != new_nodes.end())
PopulateAncestorContexts(*new_it++, &gained_selection_contexts);
// Now do a similar thing with contexts: skip common ones, and mark the ones
// that lost selection or gained selection as such.
std::set<DisplayLockContext*>::iterator lost_it =
lost_selection_contexts.begin();
std::set<DisplayLockContext*>::iterator gained_it =
gained_selection_contexts.begin();
while (lost_it != lost_selection_contexts.end() &&
gained_it != gained_selection_contexts.end()) {
if (*lost_it < *gained_it) {
(*lost_it++)->NotifySubtreeLostSelection();
} else if (*lost_it > *gained_it) {
(*gained_it++)->NotifySubtreeGainedSelection();
} else {
++lost_it;
++gained_it;
}
}
while (lost_it != lost_selection_contexts.end())
(*lost_it++)->NotifySubtreeLostSelection();
while (gained_it != gained_selection_contexts.end())
(*gained_it++)->NotifySubtreeGainedSelection();
}
void DisplayLockUtilities::SelectionRemovedFromDocument(Document& document) {
document.NotifySelectionRemovedFromDisplayLocks();
}
} // namespace blink
......@@ -95,6 +95,10 @@ class CORE_EXPORT DisplayLockUtilities {
// ensure that focused element ancestors remain unlocked for 'auto' state.
static void ElementLostFocus(Element*);
static void ElementGainedFocus(Element*);
static void SelectionChanged(const EphemeralRangeInFlatTree& old_selection,
const EphemeralRangeInFlatTree& new_selection);
static void SelectionRemovedFromDocument(Document& document);
};
} // namespace blink
......
......@@ -8497,6 +8497,15 @@ void Document::RemoveDisplayLockContext(DisplayLockContext* context) {
display_lock_contexts_.erase(context);
}
int Document::DisplayLockCount() const {
return display_lock_contexts_.size();
}
void Document::NotifySelectionRemovedFromDisplayLocks() {
for (auto context : display_lock_contexts_)
context->NotifySubtreeLostSelection();
}
Document::ScopedForceActivatableDisplayLocks
Document::GetScopedForceActivatableLocks() {
return ScopedForceActivatableDisplayLocks(this);
......
......@@ -1636,6 +1636,8 @@ class CORE_EXPORT Document : public ContainerNode,
void AddDisplayLockContext(DisplayLockContext*);
void RemoveDisplayLockContext(DisplayLockContext*);
int DisplayLockCount() const;
void NotifySelectionRemovedFromDisplayLocks();
// Manage the element's observation for display lock activation.
void RegisterDisplayLockActivationObservation(Element*);
......
......@@ -212,13 +212,21 @@ std::ostream& operator<<(std::ostream& ostream,
EphemeralRangeInFlatTree ToEphemeralRangeInFlatTree(
const EphemeralRange& range) {
// We need to update the distribution before getting the position in the flat
// tree, since that operation requires us to navigate the flat tree.
if (range.StartPosition().AnchorNode()) {
range.StartPosition()
.AnchorNode()
->UpdateDistributionForFlatTreeTraversal();
}
if (range.EndPosition().AnchorNode()) {
range.EndPosition().AnchorNode()->UpdateDistributionForFlatTreeTraversal();
}
PositionInFlatTree start = ToPositionInFlatTree(range.StartPosition());
PositionInFlatTree end = ToPositionInFlatTree(range.EndPosition());
if (start.IsNull() || end.IsNull() ||
start.GetDocument() != end.GetDocument())
return EphemeralRangeInFlatTree();
start.AnchorNode()->UpdateDistributionForFlatTreeTraversal();
end.AnchorNode()->UpdateDistributionForFlatTreeTraversal();
if (!start.IsValidFor(*start.GetDocument()) ||
!end.IsValidFor(*end.GetDocument()))
return EphemeralRangeInFlatTree();
......
......@@ -241,8 +241,19 @@ bool FrameSelection::SetSelectionDeprecated(
if (is_changed) {
AssertUserSelection(new_selection, options);
selection_editor_->SetSelectionAndEndTyping(new_selection);
DisplayLockUtilities::ActivateSelectionRangeIfNeeded(
ToEphemeralRangeInFlatTree(new_selection.ComputeRange()));
// The old selection might not be valid, and thus not iteratable. If that's
// the case, notify that all selection was removed and use an empty range as
// the old selection.
EphemeralRangeInFlatTree old_range;
if (old_selection_in_dom_tree.IsValidFor(GetDocument())) {
old_range =
ToEphemeralRangeInFlatTree(old_selection_in_dom_tree.ComputeRange());
} else {
DisplayLockUtilities::SelectionRemovedFromDocument(GetDocument());
}
DisplayLockUtilities::SelectionChanged(
old_range, ToEphemeralRangeInFlatTree(new_selection.ComputeRange()));
}
is_directional_ = options.IsDirectional();
should_shrink_next_tap_ = options.ShouldShrinkNextTap();
......
......@@ -130,6 +130,7 @@ class SelectionTemplate final {
private:
friend class SelectionEditor;
friend class FrameSelection;
enum class Direction {
kNotComputed,
......
<!doctype HTML>
<html>
<meta charset="utf8">
<title>Subtree Visibility: off-screen selection</title>
<link rel="author" title="Vladimir Levin" href="mailto:vmpstr@chromium.org">
<link rel="help" href="https://github.com/WICG/display-locking">
<meta name="assert" content="subtree-visibility auto element remains non-skipped when elements in its subtree have selection.">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style>
body, html {
padding: 0;
margin: 0;
}
.spacer {
height: 3000px;
background: lightblue;
}
#container {
background: lightgreen;
contain-intrinsic-size: 50px 100px;
subtree-visibility: auto;
}
#selectable {
width: 10px;
height: 10px;
}
</style>
<div class=spacer></div>
<div id=container>
<div id=selectable>hello</div>
</div>
<div class=spacer></div>
<script>
async_test((t) => {
const selection = window.getSelection();
const range = document.createRange();
range.selectNodeContents(selectable);
// Initially container should be 3000px offscreen with contained height 100px.
function step1() {
const r = container.getBoundingClientRect();
t.step(() => {
assert_equals(r.y, 3000, "step1 offset");
assert_equals(r.height, 100, "step1 height");
});
selection.removeAllRanges();
selection.addRange(range);
requestAnimationFrame(step2);
}
// The container has a selection so it should be smaller now, height 10px.
function step2() {
const r = container.getBoundingClientRect();
t.step(() => {
assert_equals(r.y, 3000, "step3 offset");
assert_equals(r.height, 10, "step3 height");
});
document.scrollingElement.scrollTop = 3000;
requestAnimationFrame(step3);
}
// After scrolling the container should be closer and still height 10px.
function step3() {
const r = container.getBoundingClientRect();
t.step(() => {
assert_less_than(r.y, 3000, "step2 offset");
assert_equals(r.height, 10, "step2 height");
});
document.scrollingElement.scrollTop = 0;
requestAnimationFrame(step4);
}
// Scrolling back to the top we should remain at height 10px.
function step4() {
const r = container.getBoundingClientRect();
t.step(() => {
assert_equals(r.y, 3000, "step4 offset");
assert_equals(r.height, 10, "step4 height");
});
requestAnimationFrame(step5);
}
// Repeat step4 to ensure we're in a stable situation.
function step5() {
const r = container.getBoundingClientRect();
t.step(() => {
assert_equals(r.y, 3000, "step4 offset");
assert_equals(r.height, 10, "step4 height");
});
selection.removeAllRanges();
requestAnimationFrame(step6);
}
// After removing the selection we should go back to the contained
// height of 100px.
function step6() {
const r = container.getBoundingClientRect();
t.step(() => {
assert_equals(r.y, 3000, "step5 offset");
assert_equals(r.height, 100, "step5 height");
});
t.done();
}
step1();
});
</script>
</html>
<!doctype HTML>
<html>
<meta charset="utf8">
<title>Subtree Visibility: off-screen selection</title>
<link rel="author" title="Vladimir Levin" href="mailto:vmpstr@chromium.org">
<link rel="help" href="https://github.com/WICG/display-locking">
<meta name="assert" content="subtree-visibility auto element remains non-skipped when elements in its subtree have selection.">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style>
body, html {
padding: 0;
margin: 0;
}
.spacer {
height: 3000px;
}
.container {
width: 10px;
contain-intrinsic-size: 10px 20px;
subtree-visibility: auto;
}
.child {
width: 10px;
height: 10px;
}
</style>
<div class=spacer></div>
<div id=container_1 class=container><div id=child_1 class=child>hello</div></div>
<div id=container_2 class=container><div id=child_2 class=child>hello</div></div>
<div id=container_3 class=container><div id=child_3 class=child>hello</div></div>
<div id=container_4 class=container><div id=child_4 class=child>hello</div></div>
<div id=container_5 class=container><div id=child_5 class=child>hello</div></div>
<script>
function isLocked(container) {
const height = container.getBoundingClientRect().height;
assert_true(height == 20 || height == 10);
return container.getBoundingClientRect().height == 20;
}
const selection = window.getSelection();
function resetSelection() {
selection.removeAllRanges();
assert_true(isLocked(container_1));
assert_true(isLocked(container_2));
assert_true(isLocked(container_3));
assert_true(isLocked(container_4));
assert_true(isLocked(container_5));
}
test(() => {
resetSelection();
const range = document.createRange();
range.selectNodeContents(child_2);
selection.addRange(range);
assert_true(isLocked(container_1), "1");
assert_false(isLocked(container_2), "2");
assert_true(isLocked(container_3), "3");
assert_true(isLocked(container_4), "4");
assert_true(isLocked(container_5), "5");
}, "One elements selected: ");
test(() => {
resetSelection();
const range = document.createRange();
range.selectNodeContents(child_2);
selection.addRange(range);
assert_true(isLocked(container_1), "1");
assert_false(isLocked(container_2), "2");
assert_true(isLocked(container_3), "3");
assert_true(isLocked(container_4), "4");
assert_true(isLocked(container_5), "5");
selection.extend(child_4, 0);
assert_true(isLocked(container_1), "1");
assert_false(isLocked(container_2), "2");
assert_false(isLocked(container_3), "3");
assert_false(isLocked(container_4), "4");
assert_true(isLocked(container_5), "5");
}, "Range extended to cover more elements: ");
test(() => {
resetSelection();
const range = document.createRange();
range.setStart(child_2, 0);
range.setEnd(child_4, 0);
selection.addRange(range);
assert_true(isLocked(container_1), "1");
assert_false(isLocked(container_2), "2");
assert_false(isLocked(container_3), "3");
assert_false(isLocked(container_4), "4");
assert_true(isLocked(container_5), "5");
selection.extend(child_2, 1);
assert_true(isLocked(container_1), "1");
assert_false(isLocked(container_2), "2");
assert_true(isLocked(container_3), "3");
assert_true(isLocked(container_4), "4");
assert_true(isLocked(container_5), "5");
}, "Range shrunk to cover fewer elements: ");
test(() => {
resetSelection();
const range = document.createRange();
range.setStart(child_3, 0);
range.setEnd(child_3, 0);
selection.addRange(range);
selection.extend(child_2, 0);
assert_true(isLocked(container_1), "1");
assert_false(isLocked(container_2), "2");
assert_false(isLocked(container_3), "3");
assert_true(isLocked(container_4), "4");
assert_true(isLocked(container_5), "5");
selection.extend(child_4, 0);
assert_true(isLocked(container_1), "1");
assert_true(isLocked(container_2), "2");
assert_false(isLocked(container_3), "3");
assert_false(isLocked(container_4), "4");
assert_true(isLocked(container_5), "5");
}, "Range flipped from back to front: ");
test(() => {
resetSelection();
const range = document.createRange();
range.setStart(child_3, 0);
range.setEnd(child_4, 0);
selection.addRange(range);
assert_true(isLocked(container_1), "1");
assert_true(isLocked(container_2), "2");
assert_false(isLocked(container_3), "3");
assert_false(isLocked(container_4), "4");
assert_true(isLocked(container_5), "5");
selection.extend(child_2, 0);
assert_true(isLocked(container_1), "1");
assert_false(isLocked(container_2), "2");
assert_false(isLocked(container_3), "3");
assert_true(isLocked(container_4), "4");
assert_true(isLocked(container_5), "5");
}, "Range flipped from front to back: ");
test(() => {
resetSelection();
const range = document.createRange();
range.setStart(child_1, 0);
range.setEnd(child_1, 0);
selection.addRange(range);
let state = 0;
const states = [2, 4, 3, 5, 1];
for (let i = 0; i < 10; ++i) {
const id = states[state];
selection.extend(document.getElementById(`child_${id}`), 1);
for (let check_id = 1; check_id <= 5; ++check_id) {
if (check_id <= id) {
assert_false(
isLocked(document.getElementById(`container_${check_id}`)),
`test_${i}, container_${check_id}`);
} else {
assert_true(
isLocked(document.getElementById(`container_${check_id}`)),
`test_${i}, container_${check_id}`);
}
}
state = (state + 1) % states.length;
}
}, "Range goes back and forth: ");
</script>
</html>
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