blob: 3bd23002d01accb1d1f392a25d7aad83803f13fd [file] [log] [blame]
/*
* Copyright (C) 1999 Lars Knoll (knoll@kde.org)
* Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009 Apple Inc. All rights
* reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*/
#include "core/editing/LayoutSelection.h"
#include "core/dom/Document.h"
#include "core/editing/EditingUtilities.h"
#include "core/editing/EphemeralRange.h"
#include "core/editing/FrameSelection.h"
#include "core/editing/SelectionTemplate.h"
#include "core/editing/VisiblePosition.h"
#include "core/editing/VisibleUnits.h"
#include "core/html/forms/TextControlElement.h"
#include "core/layout/LayoutText.h"
#include "core/layout/LayoutTextFragment.h"
#include "core/layout/LayoutView.h"
#include "core/layout/ng/inline/ng_offset_mapping.h"
#include "core/layout/ng/inline/ng_physical_text_fragment.h"
#include "core/paint/PaintLayer.h"
namespace blink {
SelectionPaintRange::SelectionPaintRange(LayoutObject* start_layout_object,
WTF::Optional<unsigned> start_offset,
LayoutObject* end_layout_object,
WTF::Optional<unsigned> end_offset)
: start_layout_object_(start_layout_object),
start_offset_(start_offset),
end_layout_object_(end_layout_object),
end_offset_(end_offset) {}
bool SelectionPaintRange::operator==(const SelectionPaintRange& other) const {
return start_layout_object_ == other.start_layout_object_ &&
start_offset_ == other.start_offset_ &&
end_layout_object_ == other.end_layout_object_ &&
end_offset_ == other.end_offset_;
}
LayoutObject* SelectionPaintRange::StartLayoutObject() const {
DCHECK(!IsNull());
return start_layout_object_;
}
WTF::Optional<unsigned> SelectionPaintRange::StartOffset() const {
DCHECK(!IsNull());
return start_offset_;
}
LayoutObject* SelectionPaintRange::EndLayoutObject() const {
DCHECK(!IsNull());
return end_layout_object_;
}
WTF::Optional<unsigned> SelectionPaintRange::EndOffset() const {
DCHECK(!IsNull());
return end_offset_;
}
SelectionPaintRange::Iterator::Iterator(const SelectionPaintRange* range) {
if (!range || range->IsNull()) {
current_ = nullptr;
return;
}
current_ = range->StartLayoutObject();
stop_ = range->EndLayoutObject()->NextInPreOrder();
}
LayoutObject* SelectionPaintRange::Iterator::operator*() const {
DCHECK(current_);
return current_;
}
SelectionPaintRange::Iterator& SelectionPaintRange::Iterator::operator++() {
DCHECK(current_);
current_ = current_->NextInPreOrder();
if (current_ && current_ != stop_)
return *this;
current_ = nullptr;
return *this;
}
LayoutSelection::LayoutSelection(FrameSelection& frame_selection)
: frame_selection_(&frame_selection),
has_pending_selection_(false),
paint_range_(SelectionPaintRange()) {}
enum class SelectionMode {
kNone,
kRange,
kBlockCursor,
};
static SelectionMode ComputeSelectionMode(
const FrameSelection& frame_selection) {
const SelectionInDOMTree& selection_in_dom =
frame_selection.GetSelectionInDOMTree();
if (selection_in_dom.IsRange())
return SelectionMode::kRange;
DCHECK(selection_in_dom.IsCaret());
if (!frame_selection.ShouldShowBlockCursor())
return SelectionMode::kNone;
if (IsLogicalEndOfLine(CreateVisiblePosition(selection_in_dom.Base())))
return SelectionMode::kNone;
return SelectionMode::kBlockCursor;
}
static EphemeralRangeInFlatTree CalcSelectionInFlatTree(
const FrameSelection& frame_selection) {
const SelectionInDOMTree& selection_in_dom =
frame_selection.GetSelectionInDOMTree();
switch (ComputeSelectionMode(frame_selection)) {
case SelectionMode::kNone:
return {};
case SelectionMode::kRange: {
const PositionInFlatTree& base =
ToPositionInFlatTree(selection_in_dom.Base());
const PositionInFlatTree& extent =
ToPositionInFlatTree(selection_in_dom.Extent());
if (base.IsNull() || extent.IsNull() || base == extent ||
!base.IsValidFor(frame_selection.GetDocument()) ||
!extent.IsValidFor(frame_selection.GetDocument()))
return {};
return base <= extent ? EphemeralRangeInFlatTree(base, extent)
: EphemeralRangeInFlatTree(extent, base);
}
case SelectionMode::kBlockCursor: {
const PositionInFlatTree& base =
CreateVisiblePosition(ToPositionInFlatTree(selection_in_dom.Base()))
.DeepEquivalent();
if (base.IsNull())
return {};
const PositionInFlatTree end_position =
NextPositionOf(base, PositionMoveType::kGraphemeCluster);
if (end_position.IsNull())
return {};
return base <= end_position
? EphemeralRangeInFlatTree(base, end_position)
: EphemeralRangeInFlatTree(end_position, base);
}
}
NOTREACHED();
return {};
}
// LayoutObjects each has SelectionState of kStart, kEnd, kStartAndEnd, or
// kInside.
using SelectedLayoutObjects = HashSet<LayoutObject*>;
// OldSelectedLayoutObjects is current selected LayoutObjects with
// current SelectionState which is kStart, kEnd, kStartAndEnd or kInside.
using OldSelectedLayoutObjects = HashMap<LayoutObject*, SelectionState>;
#ifndef NDEBUG
void PrintSelectedLayoutObjects(
const SelectedLayoutObjects& new_selected_objects) {
std::stringstream stream;
stream << std::endl;
for (LayoutObject* layout_object : new_selected_objects) {
PrintLayoutObjectForSelection(stream, layout_object);
stream << std::endl;
}
LOG(INFO) << stream.str();
}
void PrintOldSelectedLayoutObjects(
const OldSelectedLayoutObjects& old_selected_objects) {
std::stringstream stream;
stream << std::endl;
for (const auto& key_pair : old_selected_objects) {
LayoutObject* layout_object = key_pair.key;
SelectionState old_state = key_pair.value;
PrintLayoutObjectForSelection(stream, layout_object);
stream << " old: " << old_state << std::endl;
}
LOG(INFO) << stream.str();
}
void PrintSelectionPaintRange(const SelectionPaintRange& paint_range) {
std::stringstream stream;
stream << std::endl << "layout_objects:" << std::endl;
for (LayoutObject* layout_object : paint_range) {
PrintLayoutObjectForSelection(stream, layout_object);
stream << std::endl;
}
LOG(INFO) << stream.str();
}
void PrintSelectionStateInLayoutView(const FrameSelection& selection) {
std::stringstream stream;
stream << std::endl << "layout_objects:" << std::endl;
LayoutView* layout_view = selection.GetDocument().GetLayoutView();
for (LayoutObject* layout_object = layout_view; layout_object;
layout_object = layout_object->NextInPreOrder()) {
PrintLayoutObjectForSelection(stream, layout_object);
stream << std::endl;
}
LOG(INFO) << stream.str();
}
#endif
// This class represents a selection range in layout tree and each LayoutObject
// is SelectionState-marked.
class NewPaintRangeAndSelectedLayoutObjects {
STACK_ALLOCATED();
public:
NewPaintRangeAndSelectedLayoutObjects() = default;
NewPaintRangeAndSelectedLayoutObjects(SelectionPaintRange paint_range,
SelectedLayoutObjects selected_objects)
: paint_range_(paint_range),
selected_objects_(std::move(selected_objects)) {}
NewPaintRangeAndSelectedLayoutObjects(
NewPaintRangeAndSelectedLayoutObjects&& other) {
paint_range_ = other.paint_range_;
selected_objects_ = std::move(other.selected_objects_);
}
SelectionPaintRange PaintRange() const { return paint_range_; }
const SelectedLayoutObjects& LayoutObjects() const {
return selected_objects_;
}
private:
SelectionPaintRange paint_range_;
SelectedLayoutObjects selected_objects_;
private:
DISALLOW_COPY_AND_ASSIGN(NewPaintRangeAndSelectedLayoutObjects);
};
static void SetShouldInvalidateIfNeeded(LayoutObject* layout_object) {
if (layout_object->ShouldInvalidateSelection())
return;
layout_object->SetShouldInvalidateSelection();
// We should invalidate if ancestor of |layout_object| is LayoutSVGText
// because SVGRootInlineBoxPainter::Paint() paints selection for
// |layout_object| in/ LayoutSVGText and it is invoked when parent
// LayoutSVGText is invalidated.
// That is different from InlineTextBoxPainter::Paint() which paints
// LayoutText selection when LayoutText is invalidated.
if (!layout_object->IsSVG())
return;
for (LayoutObject* parent = layout_object->Parent(); parent;
parent = parent->Parent()) {
if (parent->IsSVGRoot())
return;
if (parent->IsSVGText()) {
if (!parent->ShouldInvalidateSelection())
parent->SetShouldInvalidateSelection();
return;
}
}
}
static void SetSelectionStateIfNeeded(LayoutObject* layout_object,
SelectionState state) {
DCHECK_NE(state, SelectionState::kContain) << layout_object;
DCHECK_NE(state, SelectionState::kNone) << layout_object;
if (layout_object->GetSelectionState() == state)
return;
// TODO(yoichio): Once we make LayoutObject::SetSelectionState() tribial, use
// it directly.
layout_object->LayoutObject::SetSelectionState(state);
// Set containing block SelectionState kContain for CSS ::selection style.
// See LayoutObject::InvalidatePaintForSelection().
for (LayoutObject* containing_block = layout_object->ContainingBlock();
containing_block;
containing_block = containing_block->ContainingBlock()) {
if (containing_block->GetSelectionState() == SelectionState::kContain)
return;
containing_block->LayoutObject::SetSelectionState(SelectionState::kContain);
}
}
// Set ShouldInvalidateSelection flag of LayoutObjects
// comparing them in |new_range| and |old_range|.
static void SetShouldInvalidateSelection(
const NewPaintRangeAndSelectedLayoutObjects& new_range,
const SelectionPaintRange& old_range,
const OldSelectedLayoutObjects& old_selected_objects) {
// We invalidate each LayoutObject in new SelectionPaintRange which
// has SelectionState of kStart, kEnd, kStartAndEnd, or kInside
// and is not in old SelectionPaintRange.
for (LayoutObject* layout_object : new_range.LayoutObjects()) {
if (old_selected_objects.Contains(layout_object))
continue;
const SelectionState new_state = layout_object->GetSelectionState();
DCHECK_NE(new_state, SelectionState::kContain) << layout_object;
DCHECK_NE(new_state, SelectionState::kNone) << layout_object;
SetShouldInvalidateIfNeeded(layout_object);
}
// For LayoutObject in old SelectionPaintRange, we invalidate LayoutObjects
// each of:
// 1. LayoutObject was painted and would not be painted.
// 2. LayoutObject was not painted and would be painted.
for (const auto& key_value : old_selected_objects) {
LayoutObject* const layout_object = key_value.key;
const SelectionState old_state = key_value.value;
const SelectionState new_state = layout_object->GetSelectionState();
if (new_state == old_state)
continue;
DCHECK(new_state != SelectionState::kNone ||
old_state != SelectionState::kNone)
<< layout_object;
DCHECK_NE(new_state, SelectionState::kContain) << layout_object;
DCHECK_NE(old_state, SelectionState::kContain) << layout_object;
SetShouldInvalidateIfNeeded(layout_object);
}
// Invalidate Selection start/end is moving on a same node.
const SelectionPaintRange& new_paint_range = new_range.PaintRange();
if (new_paint_range.IsNull() || old_range.IsNull())
return;
if (new_paint_range.StartLayoutObject()->IsText() &&
new_paint_range.StartLayoutObject() == old_range.StartLayoutObject() &&
new_paint_range.StartOffset() != old_range.StartOffset())
SetShouldInvalidateIfNeeded(new_paint_range.StartLayoutObject());
if (new_paint_range.EndLayoutObject()->IsText() &&
new_paint_range.EndLayoutObject() == old_range.EndLayoutObject() &&
new_paint_range.EndOffset() != old_range.EndOffset())
SetShouldInvalidateIfNeeded(new_paint_range.EndLayoutObject());
}
WTF::Optional<unsigned> LayoutSelection::SelectionStart() const {
DCHECK(!HasPendingSelection());
if (paint_range_.IsNull())
return WTF::nullopt;
return paint_range_.StartOffset();
}
WTF::Optional<unsigned> LayoutSelection::SelectionEnd() const {
DCHECK(!HasPendingSelection());
if (paint_range_.IsNull())
return WTF::nullopt;
return paint_range_.EndOffset();
}
static OldSelectedLayoutObjects ResetOldSelectedLayoutObjects(
const SelectionPaintRange& old_range) {
OldSelectedLayoutObjects old_selected_objects;
HashSet<LayoutObject*> containing_block_set;
for (LayoutObject* layout_object : old_range) {
const SelectionState old_state = layout_object->GetSelectionState();
if (old_state == SelectionState::kNone)
continue;
if (old_state != SelectionState::kContain)
old_selected_objects.insert(layout_object, old_state);
// TODO(yoichio): Once we make LayoutObject::SetSelectionState() trivial,
// use it directly.
layout_object->LayoutObject::SetSelectionState(SelectionState::kNone);
// Reset containing block SelectionState for CSS ::selection style.
// See LayoutObject::InvalidatePaintForSelection().
for (LayoutObject* containing_block = layout_object->ContainingBlock();
containing_block;
containing_block = containing_block->ContainingBlock()) {
if (containing_block_set.Contains(containing_block))
break;
containing_block->LayoutObject::SetSelectionState(SelectionState::kNone);
containing_block_set.insert(containing_block);
}
}
return old_selected_objects;
}
void LayoutSelection::ClearSelection() {
// For querying Layer::compositingState()
// This is correct, since destroying layout objects needs to cause eager paint
// invalidations.
DisableCompositingQueryAsserts disabler;
// Just return if the selection is already empty.
if (paint_range_.IsNull())
return;
const OldSelectedLayoutObjects& old_selected_objects =
ResetOldSelectedLayoutObjects(paint_range_);
for (LayoutObject* const layout_object : old_selected_objects.Keys())
SetShouldInvalidateIfNeeded(layout_object);
// Reset selection.
paint_range_ = SelectionPaintRange();
}
static WTF::Optional<unsigned> ComputeStartOffset(
const LayoutObject& layout_object,
const PositionInFlatTree& position) {
Node* const layout_node = layout_object.GetNode();
if (!layout_node || !layout_node->IsTextNode())
return WTF::nullopt;
if (layout_node == position.AnchorNode())
return position.OffsetInContainerNode();
return 0;
}
static WTF::Optional<unsigned> ComputeEndOffset(
const LayoutObject& layout_object,
const PositionInFlatTree& position) {
Node* const layout_node = layout_object.GetNode();
if (!layout_node || !layout_node->IsTextNode())
return WTF::nullopt;
if (layout_node == position.AnchorNode())
return position.OffsetInContainerNode();
return ToText(layout_node)->length();
}
static LayoutTextFragment* FirstLetterPartFor(LayoutObject* layout_object) {
if (!layout_object->IsText())
return nullptr;
if (!ToLayoutText(layout_object)->IsTextFragment())
return nullptr;
return ToLayoutTextFragment(const_cast<LayoutObject*>(
AssociatedLayoutObjectOf(*layout_object->GetNode(), 0)));
}
static void MarkSelected(SelectedLayoutObjects* selected_objects,
LayoutObject* layout_object,
SelectionState state) {
DCHECK(layout_object->CanBeSelectionLeaf());
SetSelectionStateIfNeeded(layout_object, state);
selected_objects->insert(layout_object);
}
static void MarkSelectedInside(SelectedLayoutObjects* selected_objects,
LayoutObject* layout_object) {
MarkSelected(selected_objects, layout_object, SelectionState::kInside);
LayoutTextFragment* const first_letter_part =
FirstLetterPartFor(layout_object);
if (!first_letter_part)
return;
MarkSelected(selected_objects, first_letter_part, SelectionState::kInside);
}
static NewPaintRangeAndSelectedLayoutObjects MarkStartAndEndInOneNode(
SelectedLayoutObjects selected_objects,
LayoutObject* layout_object,
WTF::Optional<unsigned> start_offset,
WTF::Optional<unsigned> end_offset) {
if (!layout_object->GetNode()->IsTextNode()) {
DCHECK(!start_offset.has_value());
DCHECK(!end_offset.has_value());
MarkSelected(&selected_objects, layout_object,
SelectionState::kStartAndEnd);
return {{layout_object, WTF::nullopt, layout_object, WTF::nullopt},
std::move(selected_objects)};
}
DCHECK(start_offset.has_value());
DCHECK(end_offset.has_value());
DCHECK_GE(end_offset.value(), start_offset.value());
if (start_offset.value() == end_offset.value())
return {};
LayoutTextFragment* const first_letter_part =
FirstLetterPartFor(layout_object);
if (!first_letter_part) {
MarkSelected(&selected_objects, layout_object,
SelectionState::kStartAndEnd);
return {{layout_object, start_offset, layout_object, end_offset},
std::move(selected_objects)};
}
const unsigned unsigned_start = start_offset.value();
const unsigned unsigned_end = end_offset.value();
LayoutTextFragment* const remaining_part =
ToLayoutTextFragment(layout_object);
if (unsigned_start >= remaining_part->Start()) {
// Case 1: The selection starts and ends in remaining part.
DCHECK_GT(unsigned_end, remaining_part->Start());
MarkSelected(&selected_objects, remaining_part,
SelectionState::kStartAndEnd);
return {{remaining_part, unsigned_start - remaining_part->Start(),
remaining_part, unsigned_end - remaining_part->Start()},
std::move(selected_objects)};
}
if (unsigned_end <= remaining_part->Start()) {
// Case 2: The selection starts and ends in first letter part.
MarkSelected(&selected_objects, first_letter_part,
SelectionState::kStartAndEnd);
return {{first_letter_part, start_offset, first_letter_part, end_offset},
std::move(selected_objects)};
}
// Case 3: The selection starts in first-letter part and ends in remaining
// part.
DCHECK_GT(unsigned_end, remaining_part->Start());
MarkSelected(&selected_objects, first_letter_part, SelectionState::kStart);
MarkSelected(&selected_objects, remaining_part, SelectionState::kEnd);
return {{first_letter_part, start_offset, remaining_part,
unsigned_end - remaining_part->Start()},
std::move(selected_objects)};
}
// LayoutObjectAndOffset represents start or end of SelectionPaintRange.
struct LayoutObjectAndOffset {
STACK_ALLOCATED();
LayoutObject* layout_object;
WTF::Optional<unsigned> offset;
explicit LayoutObjectAndOffset(LayoutObject* passed_layout_object)
: layout_object(passed_layout_object), offset(WTF::nullopt) {
DCHECK(passed_layout_object);
DCHECK(!passed_layout_object->GetNode()->IsTextNode());
}
LayoutObjectAndOffset(LayoutText* layout_text, unsigned passed_offset)
: layout_object(layout_text), offset(passed_offset) {
DCHECK(layout_object);
}
};
LayoutObjectAndOffset MarkStart(SelectedLayoutObjects* selected_objects,
LayoutObject* start_layout_object,
WTF::Optional<unsigned> start_offset) {
if (!start_layout_object->GetNode()->IsTextNode()) {
DCHECK(!start_offset.has_value());
MarkSelected(selected_objects, start_layout_object, SelectionState::kStart);
return LayoutObjectAndOffset(start_layout_object);
}
DCHECK(start_offset.has_value());
const unsigned unsigned_offset = start_offset.value();
LayoutText* const start_layout_text = ToLayoutText(start_layout_object);
if (unsigned_offset >= start_layout_text->TextStartOffset()) {
// |start_offset| is within |start_layout_object| whether it has first
// letter part or not.
MarkSelected(selected_objects, start_layout_object, SelectionState::kStart);
return {start_layout_text,
unsigned_offset - start_layout_text->TextStartOffset()};
}
// |start_layout_object| has first letter part and |start_offset| is within
// the part.
LayoutTextFragment* const first_letter_part =
FirstLetterPartFor(start_layout_object);
DCHECK(first_letter_part);
MarkSelected(selected_objects, first_letter_part, SelectionState::kStart);
MarkSelected(selected_objects, start_layout_text, SelectionState::kInside);
return {first_letter_part, start_offset.value()};
}
LayoutObjectAndOffset MarkEnd(SelectedLayoutObjects* selected_objects,
LayoutObject* end_layout_object,
WTF::Optional<unsigned> end_offset) {
if (!end_layout_object->GetNode()->IsTextNode()) {
DCHECK(!end_offset.has_value());
MarkSelected(selected_objects, end_layout_object, SelectionState::kEnd);
return LayoutObjectAndOffset(end_layout_object);
}
DCHECK(end_offset.has_value());
const unsigned unsigned_offset = end_offset.value();
LayoutText* const end_layout_text = ToLayoutText(end_layout_object);
if (unsigned_offset >= end_layout_text->TextStartOffset()) {
// |end_offset| is within |end_layout_object| whether it has first
// letter part or not.
MarkSelected(selected_objects, end_layout_object, SelectionState::kEnd);
if (LayoutTextFragment* const first_letter_part =
FirstLetterPartFor(end_layout_object)) {
MarkSelected(selected_objects, first_letter_part,
SelectionState::kInside);
}
return {end_layout_text,
unsigned_offset - end_layout_text->TextStartOffset()};
}
// |end_layout_object| has first letter part and |end_offset| is within
// the part.
LayoutTextFragment* const first_letter_part =
FirstLetterPartFor(end_layout_object);
DCHECK(first_letter_part);
MarkSelected(selected_objects, first_letter_part, SelectionState::kEnd);
return {first_letter_part, end_offset.value()};
}
static NewPaintRangeAndSelectedLayoutObjects MarkStartAndEndInTwoNodes(
SelectedLayoutObjects selected_objects,
LayoutObject* start_layout_object,
WTF::Optional<unsigned> start_offset,
LayoutObject* end_layout_object,
WTF::Optional<unsigned> end_offset) {
const LayoutObjectAndOffset& start =
MarkStart(&selected_objects, start_layout_object, start_offset);
const LayoutObjectAndOffset& end =
MarkEnd(&selected_objects, end_layout_object, end_offset);
return {{start.layout_object, start.offset, end.layout_object, end.offset},
std::move(selected_objects)};
}
static WTF::Optional<unsigned> GetTextContentOffset(
LayoutObject* layout_object,
WTF::Optional<unsigned> node_offset) {
DCHECK(layout_object->EnclosingNGBlockFlow());
// |layout_object| is start or end of selection and offset is only valid
// if it is LayoutText.
if (!layout_object->IsText())
return WTF::nullopt;
// There are LayoutText that selection can't be inside it(BR, WBR,
// LayoutCounter).
if (!node_offset.has_value())
return WTF::nullopt;
const Position position_in_dom(*layout_object->GetNode(),
node_offset.value());
const NGOffsetMapping* const offset_mapping =
NGOffsetMapping::GetFor(position_in_dom);
DCHECK(offset_mapping);
const WTF::Optional<unsigned>& ng_offset =
offset_mapping->GetTextContentOffset(position_in_dom);
return ng_offset;
}
static NewPaintRangeAndSelectedLayoutObjects ComputeNewPaintRange(
const NewPaintRangeAndSelectedLayoutObjects& new_range,
LayoutObject* start_layout_object,
WTF::Optional<unsigned> start_node_offset,
LayoutObject* end_layout_object,
WTF::Optional<unsigned> end_node_offset) {
if (new_range.PaintRange().IsNull())
return {};
LayoutObject* const start = new_range.PaintRange().StartLayoutObject();
// If LayoutObject is not in NG, use legacy offset.
const WTF::Optional<unsigned> start_offset =
start->EnclosingNGBlockFlow()
? GetTextContentOffset(start_layout_object, start_node_offset)
: new_range.PaintRange().StartOffset();
LayoutObject* const end = new_range.PaintRange().EndLayoutObject();
const WTF::Optional<unsigned> end_offset =
end->EnclosingNGBlockFlow()
? GetTextContentOffset(end_layout_object, end_node_offset)
: new_range.PaintRange().EndOffset();
return {{start, start_offset, end, end_offset},
std::move(new_range.LayoutObjects())};
}
// ClampOffset modifies |offset| fixed in a range of |text_fragment| start/end
// offsets.
static unsigned ClampOffset(unsigned offset,
const NGPhysicalTextFragment& text_fragment) {
return std::min(std::max(offset, text_fragment.StartOffset()),
text_fragment.EndOffset());
}
std::pair<unsigned, unsigned> LayoutSelection::SelectionStartEndForNG(
const NGPhysicalTextFragment& text_fragment) {
// FrameSelection holds selection offsets in layout block flow at
// LayoutSelection::Commit() if selection starts/ends within Text that
// each LayoutObject::SelectionState indicates.
// These offset can out of |text_fragment| because SelectionState is of each
// LayoutText and not |text_fragment|.
switch (text_fragment.GetLayoutObject()->GetSelectionState()) {
case SelectionState::kStart: {
DCHECK(SelectionStart().has_value());
unsigned start_in_block = SelectionStart().value_or(0);
return {ClampOffset(start_in_block, text_fragment),
text_fragment.EndOffset()};
}
case SelectionState::kEnd: {
DCHECK(SelectionEnd().has_value());
unsigned end_in_block =
SelectionEnd().value_or(text_fragment.EndOffset());
return {text_fragment.StartOffset(),
ClampOffset(end_in_block, text_fragment)};
}
case SelectionState::kStartAndEnd: {
DCHECK(SelectionStart().has_value());
DCHECK(SelectionEnd().has_value());
unsigned start_in_block = SelectionStart().value_or(0);
unsigned end_in_block =
SelectionEnd().value_or(text_fragment.EndOffset());
return {ClampOffset(start_in_block, text_fragment),
ClampOffset(end_in_block, text_fragment)};
}
case SelectionState::kInside:
return {text_fragment.StartOffset(), text_fragment.EndOffset()};
default:
// This block is not included in selection.
return {0, 0};
}
}
static NewPaintRangeAndSelectedLayoutObjects
CalcSelectionRangeAndSetSelectionState(const FrameSelection& frame_selection) {
const SelectionInDOMTree& selection_in_dom =
frame_selection.GetSelectionInDOMTree();
if (selection_in_dom.IsNone())
return {};
const EphemeralRangeInFlatTree& selection =
CalcSelectionInFlatTree(frame_selection);
if (selection.IsCollapsed() || frame_selection.IsHidden())
return {};
// Find first/last visible LayoutObject while
// marking SelectionState and collecting invalidation candidate LayoutObjects.
LayoutObject* start_layout_object = nullptr;
LayoutObject* end_layout_object = nullptr;
SelectedLayoutObjects selected_objects;
for (const Node& node : selection.Nodes()) {
LayoutObject* const layout_object = node.GetLayoutObject();
if (!layout_object || !layout_object->CanBeSelectionLeaf())
continue;
if (!start_layout_object) {
DCHECK(!end_layout_object);
start_layout_object = end_layout_object = layout_object;
continue;
}
// In this loop, |end_layout_object| is pointing current last candidate
// LayoutObject and if it is not start and we find next, we mark the
// current one as kInside.
if (end_layout_object != start_layout_object)
MarkSelectedInside(&selected_objects, end_layout_object);
end_layout_object = layout_object;
}
// No valid LayOutObject found.
if (!start_layout_object) {
DCHECK(!end_layout_object);
return {};
}
// Compute offset. It has value iff start/end is text.
const WTF::Optional<unsigned> start_offset = ComputeStartOffset(
*start_layout_object, selection.StartPosition().ToOffsetInAnchor());
const WTF::Optional<unsigned> end_offset = ComputeEndOffset(
*end_layout_object, selection.EndPosition().ToOffsetInAnchor());
NewPaintRangeAndSelectedLayoutObjects new_range =
start_layout_object == end_layout_object
? MarkStartAndEndInOneNode(std::move(selected_objects),
start_layout_object, start_offset,
end_offset)
: MarkStartAndEndInTwoNodes(std::move(selected_objects),
start_layout_object, start_offset,
end_layout_object, end_offset);
if (!RuntimeEnabledFeatures::LayoutNGPaintFragmentsEnabled())
return new_range;
return ComputeNewPaintRange(new_range, start_layout_object, start_offset,
end_layout_object, end_offset);
}
void LayoutSelection::SetHasPendingSelection() {
has_pending_selection_ = true;
}
void LayoutSelection::Commit() {
if (!HasPendingSelection())
return;
has_pending_selection_ = false;
DCHECK(!frame_selection_->GetDocument().NeedsLayoutTreeUpdate());
DCHECK_GE(frame_selection_->GetDocument().Lifecycle().GetState(),
DocumentLifecycle::kLayoutClean);
DocumentLifecycle::DisallowTransitionScope disallow_transition(
frame_selection_->GetDocument().Lifecycle());
const OldSelectedLayoutObjects& old_selected_objects =
ResetOldSelectedLayoutObjects(paint_range_);
const NewPaintRangeAndSelectedLayoutObjects& new_range =
CalcSelectionRangeAndSetSelectionState(*frame_selection_);
DCHECK(frame_selection_->GetDocument().GetLayoutView()->GetFrameView());
SetShouldInvalidateSelection(new_range, paint_range_, old_selected_objects);
paint_range_ = new_range.PaintRange();
if (paint_range_.IsNull())
return;
// TODO(yoichio): Remove this if state.
// This SelectionState reassignment is ad-hoc patch for
// prohibiting use-after-free(crbug.com/752715).
// LayoutText::setSelectionState(state) propergates |state| to ancestor
// LayoutObjects, which can accidentally change start/end LayoutObject state
// then LayoutObject::IsSelectionBorder() returns false although we should
// clear selection at LayoutObject::WillBeRemoved().
// We should make LayoutObject::setSelectionState() trivial and remove
// such propagation or at least do it in LayoutSelection.
if ((paint_range_.StartLayoutObject()->GetSelectionState() !=
SelectionState::kStart &&
paint_range_.StartLayoutObject()->GetSelectionState() !=
SelectionState::kStartAndEnd) ||
(paint_range_.EndLayoutObject()->GetSelectionState() !=
SelectionState::kEnd &&
paint_range_.EndLayoutObject()->GetSelectionState() !=
SelectionState::kStartAndEnd)) {
if (paint_range_.StartLayoutObject() == paint_range_.EndLayoutObject()) {
paint_range_.StartLayoutObject()->LayoutObject::SetSelectionState(
SelectionState::kStartAndEnd);
} else {
paint_range_.StartLayoutObject()->LayoutObject::SetSelectionState(
SelectionState::kStart);
paint_range_.EndLayoutObject()->LayoutObject::SetSelectionState(
SelectionState::kEnd);
}
}
// TODO(yoichio): If start == end, they should be kStartAndEnd.
// If not, start.SelectionState == kStart and vice versa.
DCHECK(paint_range_.StartLayoutObject()->GetSelectionState() ==
SelectionState::kStart ||
paint_range_.StartLayoutObject()->GetSelectionState() ==
SelectionState::kStartAndEnd);
DCHECK(paint_range_.EndLayoutObject()->GetSelectionState() ==
SelectionState::kEnd ||
paint_range_.EndLayoutObject()->GetSelectionState() ==
SelectionState::kStartAndEnd);
}
void LayoutSelection::OnDocumentShutdown() {
has_pending_selection_ = false;
paint_range_ = SelectionPaintRange();
}
static LayoutRect SelectionRectForLayoutObject(const LayoutObject* object) {
if (!object->IsRooted())
return LayoutRect();
if (!object->CanUpdateSelectionOnRootLineBoxes())
return LayoutRect();
return object->SelectionRectInViewCoordinates();
}
IntRect LayoutSelection::SelectionBounds() {
Commit();
if (paint_range_.IsNull())
return IntRect();
// Create a single bounding box rect that encloses the whole selection.
LayoutRect selected_rect;
for (LayoutObject* layout_object : paint_range_) {
const SelectionState state = layout_object->GetSelectionState();
if (state == SelectionState::kContain || state == SelectionState::kNone)
continue;
selected_rect.Unite(SelectionRectForLayoutObject(layout_object));
}
return PixelSnappedIntRect(selected_rect);
}
void LayoutSelection::InvalidatePaintForSelection() {
if (paint_range_.IsNull())
return;
for (LayoutObject* runner : paint_range_) {
if (runner->GetSelectionState() == SelectionState::kNone)
continue;
runner->SetShouldInvalidateSelection();
}
}
void LayoutSelection::Trace(blink::Visitor* visitor) {
visitor->Trace(frame_selection_);
}
void PrintLayoutObjectForSelection(std::ostream& ostream,
LayoutObject* layout_object) {
if (!layout_object) {
ostream << "<null>";
return;
}
ostream << (void*)layout_object << ' ' << layout_object->GetNode()
<< ", state:" << layout_object->GetSelectionState()
<< (layout_object->ShouldInvalidateSelection() ? ", ShouldInvalidate"
: ", NotInvalidate");
}
#ifndef NDEBUG
void ShowLayoutObjectForSelection(LayoutObject* layout_object) {
std::stringstream stream;
PrintLayoutObjectForSelection(stream, layout_object);
LOG(INFO) << '\n' << stream.str();
}
#endif
} // namespace blink