blob: 15d75e08ecbf3a2a8186859c2301fe862a212a87 [file] [log] [blame]
/*
* Copyright (C) 2004, 2005, 2006, 2007, 2008, 2009, 2010 Apple Inc. All rights
* reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE COMPUTER, INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "third_party/blink/renderer/core/editing/selection_modifier.h"
#include "third_party/blink/renderer/core/editing/bidi_adjustment.h"
#include "third_party/blink/renderer/core/editing/editing_behavior.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/editing/editor.h"
#include "third_party/blink/renderer/core/editing/ephemeral_range.h"
#include "third_party/blink/renderer/core/editing/inline_box_position.h"
#include "third_party/blink/renderer/core/editing/local_caret_rect.h"
#include "third_party/blink/renderer/core/editing/ng_flat_tree_shorthands.h"
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/editing/visible_position.h"
#include "third_party/blink/renderer/core/editing/visible_units.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/layout/layout_block.h"
#include "third_party/blink/renderer/core/layout/line/inline_text_box.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_caret_position.h"
#include "third_party/blink/renderer/core/layout/ng/inline/ng_offset_mapping.h"
#include "third_party/blink/renderer/core/layout/ng/ng_physical_fragment.h"
#include "third_party/blink/renderer/core/page/spatial_navigation.h"
namespace blink {
namespace {
// There are some cases where |SelectionModifier::ModifyWithPageGranularity()|
// enters an infinite loop. Work around it by hard-limiting the iteration.
const unsigned kMaxIterationForPageGranularityMovement = 1024;
VisiblePositionInFlatTree LeftBoundaryOfLine(const VisiblePositionInFlatTree& c,
TextDirection direction) {
DCHECK(c.IsValid()) << c;
return direction == TextDirection::kLtr ? LogicalStartOfLine(c)
: LogicalEndOfLine(c);
}
VisiblePositionInFlatTree RightBoundaryOfLine(
const VisiblePositionInFlatTree& c,
TextDirection direction) {
DCHECK(c.IsValid()) << c;
return direction == TextDirection::kLtr ? LogicalEndOfLine(c)
: LogicalStartOfLine(c);
}
} // namespace
static bool InSameParagraph(const VisiblePositionInFlatTree& a,
const VisiblePositionInFlatTree& b,
EditingBoundaryCrossingRule boundary_crossing_rule =
kCannotCrossEditingBoundary) {
DCHECK(a.IsValid()) << a;
DCHECK(b.IsValid()) << b;
return a.IsNotNull() &&
StartOfParagraph(a, boundary_crossing_rule).DeepEquivalent() ==
StartOfParagraph(b, boundary_crossing_rule).DeepEquivalent();
}
// static
VisiblePositionInFlatTree SelectionModifier::PreviousParagraphPosition(
const VisiblePositionInFlatTree& passed_position,
LayoutUnit x_point) {
VisiblePositionInFlatTree position = passed_position;
do {
DCHECK(position.IsValid()) << position;
const VisiblePositionInFlatTree& new_position = CreateVisiblePosition(
PreviousLinePosition(position.ToPositionWithAffinity(), x_point));
if (new_position.IsNull() ||
new_position.DeepEquivalent() == position.DeepEquivalent())
break;
position = new_position;
} while (InSameParagraph(passed_position, position));
return position;
}
// static
VisiblePositionInFlatTree SelectionModifier::NextParagraphPosition(
const VisiblePositionInFlatTree& passed_position,
LayoutUnit x_point) {
VisiblePositionInFlatTree position = passed_position;
do {
DCHECK(position.IsValid()) << position;
const VisiblePositionInFlatTree& new_position = CreateVisiblePosition(
NextLinePosition(position.ToPositionWithAffinity(), x_point));
if (new_position.IsNull() ||
new_position.DeepEquivalent() == position.DeepEquivalent())
break;
position = new_position;
} while (InSameParagraph(passed_position, position));
return position;
}
LayoutUnit NoXPosForVerticalArrowNavigation() {
return LayoutUnit::Min();
}
bool SelectionModifier::ShouldAlwaysUseDirectionalSelection(
const LocalFrame& frame) {
return frame.GetEditor().Behavior().ShouldConsiderSelectionAsDirectional();
}
SelectionModifier::SelectionModifier(
const LocalFrame& frame,
const SelectionInDOMTree& selection,
LayoutUnit x_pos_for_vertical_arrow_navigation)
: frame_(&frame),
current_selection_(ConvertToSelectionInFlatTree(selection)),
x_pos_for_vertical_arrow_navigation_(
x_pos_for_vertical_arrow_navigation) {}
SelectionModifier::SelectionModifier(const LocalFrame& frame,
const SelectionInDOMTree& selection)
: SelectionModifier(frame, selection, NoXPosForVerticalArrowNavigation()) {}
VisibleSelection SelectionModifier::Selection() const {
return CreateVisibleSelection(
ConvertToSelectionInDOMTree(current_selection_));
}
static VisiblePositionInFlatTree ComputeVisibleExtent(
const VisibleSelectionInFlatTree& visible_selection) {
return CreateVisiblePosition(visible_selection.Extent(),
visible_selection.Affinity());
}
TextDirection SelectionModifier::DirectionOfEnclosingBlock() const {
const PositionInFlatTree& selection_extent = selection_.Extent();
// TODO(editing-dev): Check for PositionInFlatTree::IsNotNull is an easy fix
// for few editing/ web tests, that didn't expect that (e.g.
// editing/selection/extend-byline-withfloat.html).
// That should be fixed in a more appropriate manner.
// We should either have SelectionModifier aborted earlier for null selection,
// or do not allow null selection in SelectionModifier at all.
return selection_extent.IsNotNull()
? DirectionOfEnclosingBlockOf(selection_extent)
: TextDirection::kLtr;
}
namespace {
base::Optional<TextDirection> DirectionAt(
const PositionInFlatTreeWithAffinity& position) {
if (position.IsNull())
return base::nullopt;
const PositionInFlatTreeWithAffinity adjusted =
ComputeInlineAdjustedPosition(position);
if (adjusted.IsNull())
return base::nullopt;
if (NGInlineFormattingContextOf(adjusted.GetPosition())) {
const NGInlineCursor& cursor = ComputeNGCaretPosition(adjusted).cursor;
if (cursor)
return cursor.Current().ResolvedDirection();
return base::nullopt;
}
if (const InlineBox* box =
ComputeInlineBoxPositionForInlineAdjustedPosition(adjusted)
.inline_box)
return box->Direction();
return base::nullopt;
}
// TODO(xiaochengh): Deduplicate code with |DirectionAt()|.
base::Optional<TextDirection> LineDirectionAt(
const PositionInFlatTreeWithAffinity& position) {
if (position.IsNull())
return base::nullopt;
const PositionInFlatTreeWithAffinity adjusted =
ComputeInlineAdjustedPosition(position);
if (adjusted.IsNull())
return base::nullopt;
if (NGInlineFormattingContextOf(adjusted.GetPosition())) {
NGInlineCursor line = ComputeNGCaretPosition(adjusted).cursor;
if (!line)
return base::nullopt;
line.MoveToContainingLine();
return line.Current().BaseDirection();
}
if (const InlineBox* box =
ComputeInlineBoxPositionForInlineAdjustedPosition(adjusted)
.inline_box) {
return ParagraphDirectionOf(*box);
}
return base::nullopt;
}
TextDirection DirectionOf(const VisibleSelectionInFlatTree& visible_selection) {
base::Optional<TextDirection> maybe_start_direction =
DirectionAt(visible_selection.VisibleStart().ToPositionWithAffinity());
base::Optional<TextDirection> maybe_end_direction =
DirectionAt(visible_selection.VisibleEnd().ToPositionWithAffinity());
if (maybe_start_direction.has_value() && maybe_end_direction.has_value() &&
maybe_start_direction.value() == maybe_end_direction.value())
return maybe_start_direction.value();
return DirectionOfEnclosingBlockOf(visible_selection.Extent());
}
} // namespace
TextDirection SelectionModifier::DirectionOfSelection() const {
return DirectionOf(selection_);
}
TextDirection SelectionModifier::LineDirectionOfExtent() const {
return LineDirectionAt(selection_.VisibleExtent().ToPositionWithAffinity())
.value_or(DirectionOfEnclosingBlockOf(selection_.Extent()));
}
static bool IsBaseStart(const VisibleSelectionInFlatTree& visible_selection,
SelectionModifyDirection direction) {
switch (direction) {
case SelectionModifyDirection::kRight:
return DirectionOf(visible_selection) == TextDirection::kLtr;
case SelectionModifyDirection::kForward:
return true;
case SelectionModifyDirection::kLeft:
return DirectionOf(visible_selection) != TextDirection::kLtr;
case SelectionModifyDirection::kBackward:
return false;
}
NOTREACHED() << "We should handle " << static_cast<int>(direction);
return true;
}
// This function returns |VisibleSelectionInFlatTree| from start and end
// position of current_selection_'s |VisibleSelectionInFlatTree| with
// |direction| and ordering of base and extent to handle base/extent don't match
// to start/end, e.g. granularity
// != character, and start/end adjustment in
// |VisibleSelectionInFlatTree::validate()| for range selection.
VisibleSelectionInFlatTree SelectionModifier::PrepareToModifySelection(
SelectionModifyAlteration alter,
SelectionModifyDirection direction) const {
const VisibleSelectionInFlatTree& visible_selection =
CreateVisibleSelection(current_selection_);
if (alter != SelectionModifyAlteration::kExtend)
return visible_selection;
if (visible_selection.IsNone())
return visible_selection;
const EphemeralRangeInFlatTree& range =
visible_selection.AsSelection().ComputeRange();
if (range.IsCollapsed())
return visible_selection;
SelectionInFlatTree::Builder builder;
// Make base and extent match start and end so we extend the user-visible
// selection. This only matters for cases where base and extend point to
// different positions than start and end (e.g. after a double-click to
// select a word).
const bool base_is_start = selection_is_directional_
? visible_selection.IsBaseFirst()
: IsBaseStart(visible_selection, direction);
if (base_is_start)
builder.SetAsForwardSelection(range);
else
builder.SetAsBackwardSelection(range);
return CreateVisibleSelection(builder.Build());
}
VisiblePositionInFlatTree SelectionModifier::PositionForPlatform(
bool is_get_start) const {
Settings* settings = GetFrame().GetSettings();
if (settings && settings->GetEditingBehaviorType() ==
mojom::blink::EditingBehavior::kEditingMacBehavior)
return is_get_start ? selection_.VisibleStart() : selection_.VisibleEnd();
// Linux and Windows always extend selections from the extent endpoint.
// FIXME: VisibleSelectionInFlatTree should be fixed to ensure as an invariant
// that base/extent always point to the same nodes as start/end, but which
// points to which depends on the value of isBaseFirst. Then this can be
// changed to just return selection_.extent().
return selection_.IsBaseFirst() ? selection_.VisibleEnd()
: selection_.VisibleStart();
}
VisiblePositionInFlatTree SelectionModifier::StartForPlatform() const {
return PositionForPlatform(true);
}
VisiblePositionInFlatTree SelectionModifier::EndForPlatform() const {
return PositionForPlatform(false);
}
PositionInFlatTree SelectionModifier::NextWordPositionForPlatform(
const PositionInFlatTree& original_position) {
const PlatformWordBehavior platform_word_behavior =
GetFrame().GetEditor().Behavior().ShouldSkipSpaceWhenMovingRight()
? PlatformWordBehavior::kWordSkipSpaces
: PlatformWordBehavior::kWordDontSkipSpaces;
// Next word position can't be upstream.
const PositionInFlatTree position_after_current_word =
NextWordPosition(original_position, platform_word_behavior).GetPosition();
return position_after_current_word;
}
static VisiblePositionInFlatTree AdjustForwardPositionForUserSelectAll(
const VisiblePositionInFlatTree& position) {
Node* const root_user_select_all = EditingStrategy::RootUserSelectAllForNode(
position.DeepEquivalent().AnchorNode());
if (!root_user_select_all)
return position;
return CreateVisiblePosition(MostForwardCaretPosition(
PositionInFlatTree::AfterNode(*root_user_select_all),
kCanCrossEditingBoundary));
}
static VisiblePositionInFlatTree AdjustBackwardPositionForUserSelectAll(
const VisiblePositionInFlatTree& position) {
Node* const root_user_select_all = EditingStrategy::RootUserSelectAllForNode(
position.DeepEquivalent().AnchorNode());
if (!root_user_select_all)
return position;
return CreateVisiblePosition(MostBackwardCaretPosition(
PositionInFlatTree::BeforeNode(*root_user_select_all),
kCanCrossEditingBoundary));
}
VisiblePositionInFlatTree SelectionModifier::ModifyExtendingRightInternal(
TextGranularity granularity) {
// The difference between modifyExtendingRight and modifyExtendingForward is:
// modifyExtendingForward always extends forward logically.
// modifyExtendingRight behaves the same as modifyExtendingForward except for
// extending character or word, it extends forward logically if the enclosing
// block is LTR direction, but it extends backward logically if the enclosing
// block is RTL direction.
switch (granularity) {
case TextGranularity::kCharacter:
if (DirectionOfEnclosingBlock() == TextDirection::kLtr) {
return NextPositionOf(ComputeVisibleExtent(selection_),
kCanSkipOverEditingBoundary);
}
return PreviousPositionOf(ComputeVisibleExtent(selection_),
kCanSkipOverEditingBoundary);
case TextGranularity::kWord:
if (DirectionOfEnclosingBlock() == TextDirection::kLtr) {
return CreateVisiblePosition(NextWordPositionForPlatform(
ComputeVisibleExtent(selection_).DeepEquivalent()));
}
return CreateVisiblePosition(PreviousWordPosition(
ComputeVisibleExtent(selection_).DeepEquivalent()));
case TextGranularity::kLineBoundary:
if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
return ModifyExtendingForwardInternal(granularity);
return ModifyExtendingBackwardInternal(granularity);
case TextGranularity::kSentence:
case TextGranularity::kLine:
case TextGranularity::kParagraph:
case TextGranularity::kSentenceBoundary:
case TextGranularity::kParagraphBoundary:
case TextGranularity::kDocumentBoundary:
// TODO(editing-dev): implement all of the above?
return ModifyExtendingForwardInternal(granularity);
}
NOTREACHED() << static_cast<int>(granularity);
return VisiblePositionInFlatTree();
}
VisiblePositionInFlatTree SelectionModifier::ModifyExtendingRight(
TextGranularity granularity) {
const VisiblePositionInFlatTree& pos =
ModifyExtendingRightInternal(granularity);
if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
return AdjustForwardPositionForUserSelectAll(pos);
return AdjustBackwardPositionForUserSelectAll(pos);
}
VisiblePositionInFlatTree SelectionModifier::ModifyExtendingForwardInternal(
TextGranularity granularity) {
switch (granularity) {
case TextGranularity::kCharacter:
return NextPositionOf(ComputeVisibleExtent(selection_),
kCanSkipOverEditingBoundary);
case TextGranularity::kWord:
return CreateVisiblePosition(NextWordPositionForPlatform(
ComputeVisibleExtent(selection_).DeepEquivalent()));
case TextGranularity::kSentence:
return CreateVisiblePosition(
NextSentencePosition(
ComputeVisibleExtent(selection_).DeepEquivalent()),
TextAffinity::kUpstreamIfPossible);
case TextGranularity::kLine: {
const VisiblePositionInFlatTree& pos = ComputeVisibleExtent(selection_);
DCHECK(pos.IsValid()) << pos;
return CreateVisiblePosition(NextLinePosition(
pos.ToPositionWithAffinity(),
LineDirectionPointForBlockDirectionNavigation(selection_.Extent())));
}
case TextGranularity::kParagraph:
return NextParagraphPosition(
ComputeVisibleExtent(selection_),
LineDirectionPointForBlockDirectionNavigation(selection_.Extent()));
case TextGranularity::kSentenceBoundary:
return EndOfSentence(EndForPlatform());
case TextGranularity::kLineBoundary:
return LogicalEndOfLine(EndForPlatform());
case TextGranularity::kParagraphBoundary:
return EndOfParagraph(EndForPlatform());
case TextGranularity::kDocumentBoundary: {
const VisiblePositionInFlatTree& pos = EndForPlatform();
if (IsEditablePosition(pos.DeepEquivalent())) {
DCHECK(pos.IsValid()) << pos;
return CreateVisiblePosition(
EndOfEditableContent(pos.DeepEquivalent()));
}
return EndOfDocument(pos);
}
}
NOTREACHED() << static_cast<int>(granularity);
return VisiblePositionInFlatTree();
}
VisiblePositionInFlatTree SelectionModifier::ModifyExtendingForward(
TextGranularity granularity) {
const VisiblePositionInFlatTree pos =
ModifyExtendingForwardInternal(granularity);
if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
return AdjustForwardPositionForUserSelectAll(pos);
return AdjustBackwardPositionForUserSelectAll(pos);
}
VisiblePositionInFlatTree SelectionModifier::ModifyMovingRight(
TextGranularity granularity) {
switch (granularity) {
case TextGranularity::kCharacter:
if (!selection_.IsRange()) {
if (LineDirectionOfExtent() == TextDirection::kLtr)
return ModifyMovingForward(granularity);
return ModifyMovingBackward(granularity);
}
if (DirectionOfSelection() == TextDirection::kLtr)
return CreateVisiblePosition(selection_.End(), selection_.Affinity());
return CreateVisiblePosition(selection_.Start(), selection_.Affinity());
case TextGranularity::kWord:
if (LineDirectionOfExtent() == TextDirection::kLtr)
return ModifyMovingForward(granularity);
return ModifyMovingBackward(granularity);
case TextGranularity::kSentence:
case TextGranularity::kLine:
case TextGranularity::kParagraph:
case TextGranularity::kSentenceBoundary:
case TextGranularity::kParagraphBoundary:
case TextGranularity::kDocumentBoundary:
// TODO(editing-dev): Implement all of the above.
return ModifyMovingForward(granularity);
case TextGranularity::kLineBoundary:
return RightBoundaryOfLine(StartForPlatform(),
DirectionOfEnclosingBlock());
}
NOTREACHED() << static_cast<int>(granularity);
return VisiblePositionInFlatTree();
}
VisiblePositionInFlatTree SelectionModifier::ModifyMovingForward(
TextGranularity granularity) {
// TODO(editing-dev): Stay in editable content for the less common
// granularities.
switch (granularity) {
case TextGranularity::kCharacter:
if (selection_.IsRange())
return CreateVisiblePosition(selection_.End(), selection_.Affinity());
return NextPositionOf(ComputeVisibleExtent(selection_),
kCanSkipOverEditingBoundary);
case TextGranularity::kWord:
return CreateVisiblePosition(NextWordPositionForPlatform(
ComputeVisibleExtent(selection_).DeepEquivalent()));
case TextGranularity::kSentence:
return CreateVisiblePosition(
NextSentencePosition(
ComputeVisibleExtent(selection_).DeepEquivalent()),
TextAffinity::kUpstreamIfPossible);
case TextGranularity::kLine: {
// down-arrowing from a range selection that ends at the start of a line
// needs to leave the selection at that line start (no need to call
// nextLinePosition!)
const VisiblePositionInFlatTree& pos = EndForPlatform();
if (selection_.IsRange() && IsStartOfLine(pos))
return pos;
DCHECK(pos.IsValid()) << pos;
return CreateVisiblePosition(NextLinePosition(
pos.ToPositionWithAffinity(),
LineDirectionPointForBlockDirectionNavigation(selection_.Start())));
}
case TextGranularity::kParagraph:
return NextParagraphPosition(
EndForPlatform(),
LineDirectionPointForBlockDirectionNavigation(selection_.Start()));
case TextGranularity::kSentenceBoundary:
return EndOfSentence(EndForPlatform());
case TextGranularity::kLineBoundary:
return LogicalEndOfLine(EndForPlatform());
case TextGranularity::kParagraphBoundary:
return EndOfParagraph(EndForPlatform());
case TextGranularity::kDocumentBoundary: {
const VisiblePositionInFlatTree& pos = EndForPlatform();
if (IsEditablePosition(pos.DeepEquivalent())) {
DCHECK(pos.IsValid()) << pos;
return CreateVisiblePosition(
EndOfEditableContent(pos.DeepEquivalent()));
}
return EndOfDocument(pos);
}
}
NOTREACHED() << static_cast<int>(granularity);
return VisiblePositionInFlatTree();
}
VisiblePositionInFlatTree SelectionModifier::ModifyExtendingLeftInternal(
TextGranularity granularity) {
// The difference between modifyExtendingLeft and modifyExtendingBackward is:
// modifyExtendingBackward always extends backward logically.
// modifyExtendingLeft behaves the same as modifyExtendingBackward except for
// extending character or word, it extends backward logically if the enclosing
// block is LTR direction, but it extends forward logically if the enclosing
// block is RTL direction.
switch (granularity) {
case TextGranularity::kCharacter:
if (DirectionOfEnclosingBlock() == TextDirection::kLtr) {
return PreviousPositionOf(ComputeVisibleExtent(selection_),
kCanSkipOverEditingBoundary);
}
return NextPositionOf(ComputeVisibleExtent(selection_),
kCanSkipOverEditingBoundary);
case TextGranularity::kWord:
if (DirectionOfEnclosingBlock() == TextDirection::kLtr) {
return CreateVisiblePosition(PreviousWordPosition(
ComputeVisibleExtent(selection_).DeepEquivalent()));
}
return CreateVisiblePosition(NextWordPositionForPlatform(
ComputeVisibleExtent(selection_).DeepEquivalent()));
case TextGranularity::kLineBoundary:
if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
return ModifyExtendingBackwardInternal(granularity);
return ModifyExtendingForwardInternal(granularity);
case TextGranularity::kSentence:
case TextGranularity::kLine:
case TextGranularity::kParagraph:
case TextGranularity::kSentenceBoundary:
case TextGranularity::kParagraphBoundary:
case TextGranularity::kDocumentBoundary:
return ModifyExtendingBackwardInternal(granularity);
}
NOTREACHED() << static_cast<int>(granularity);
return VisiblePositionInFlatTree();
}
VisiblePositionInFlatTree SelectionModifier::ModifyExtendingLeft(
TextGranularity granularity) {
const VisiblePositionInFlatTree& pos =
ModifyExtendingLeftInternal(granularity);
if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
return AdjustBackwardPositionForUserSelectAll(pos);
return AdjustForwardPositionForUserSelectAll(pos);
}
VisiblePositionInFlatTree SelectionModifier::ModifyExtendingBackwardInternal(
TextGranularity granularity) {
// Extending a selection backward by word or character from just after a table
// selects the table. This "makes sense" from the user perspective, esp. when
// deleting. It was done here instead of in VisiblePositionInFlatTree because
// we want VPs to iterate over everything.
switch (granularity) {
case TextGranularity::kCharacter:
return PreviousPositionOf(ComputeVisibleExtent(selection_),
kCanSkipOverEditingBoundary);
case TextGranularity::kWord:
return CreateVisiblePosition(PreviousWordPosition(
ComputeVisibleExtent(selection_).DeepEquivalent()));
case TextGranularity::kSentence:
return CreateVisiblePosition(PreviousSentencePosition(
ComputeVisibleExtent(selection_).DeepEquivalent()));
case TextGranularity::kLine: {
const VisiblePositionInFlatTree& pos = ComputeVisibleExtent(selection_);
DCHECK(pos.IsValid()) << pos;
return CreateVisiblePosition(PreviousLinePosition(
pos.ToPositionWithAffinity(),
LineDirectionPointForBlockDirectionNavigation(selection_.Extent())));
}
case TextGranularity::kParagraph:
return PreviousParagraphPosition(
ComputeVisibleExtent(selection_),
LineDirectionPointForBlockDirectionNavigation(selection_.Extent()));
case TextGranularity::kSentenceBoundary:
return CreateVisiblePosition(
StartOfSentencePosition(StartForPlatform().DeepEquivalent()));
case TextGranularity::kLineBoundary:
return LogicalStartOfLine(StartForPlatform());
case TextGranularity::kParagraphBoundary:
return StartOfParagraph(StartForPlatform());
case TextGranularity::kDocumentBoundary: {
const VisiblePositionInFlatTree pos = StartForPlatform();
if (IsEditablePosition(pos.DeepEquivalent())) {
DCHECK(pos.IsValid()) << pos;
return CreateVisiblePosition(
StartOfEditableContent(pos.DeepEquivalent()));
}
return CreateVisiblePosition(StartOfDocument(pos.DeepEquivalent()));
}
}
NOTREACHED() << static_cast<int>(granularity);
return VisiblePositionInFlatTree();
}
VisiblePositionInFlatTree SelectionModifier::ModifyExtendingBackward(
TextGranularity granularity) {
const VisiblePositionInFlatTree pos =
ModifyExtendingBackwardInternal(granularity);
if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
return AdjustBackwardPositionForUserSelectAll(pos);
return AdjustForwardPositionForUserSelectAll(pos);
}
VisiblePositionInFlatTree SelectionModifier::ModifyMovingLeft(
TextGranularity granularity) {
switch (granularity) {
case TextGranularity::kCharacter:
if (!selection_.IsRange()) {
if (LineDirectionOfExtent() == TextDirection::kLtr)
return ModifyMovingBackward(granularity);
return ModifyMovingForward(granularity);
}
if (DirectionOfSelection() == TextDirection::kLtr)
return CreateVisiblePosition(selection_.Start(), selection_.Affinity());
return CreateVisiblePosition(selection_.End(), selection_.Affinity());
case TextGranularity::kWord:
if (LineDirectionOfExtent() == TextDirection::kLtr)
return ModifyMovingBackward(granularity);
return ModifyMovingForward(granularity);
case TextGranularity::kSentence:
case TextGranularity::kLine:
case TextGranularity::kParagraph:
case TextGranularity::kSentenceBoundary:
case TextGranularity::kParagraphBoundary:
case TextGranularity::kDocumentBoundary:
// FIXME: Implement all of the above.
return ModifyMovingBackward(granularity);
case TextGranularity::kLineBoundary:
return LeftBoundaryOfLine(StartForPlatform(),
DirectionOfEnclosingBlock());
}
NOTREACHED() << static_cast<int>(granularity);
return VisiblePositionInFlatTree();
}
VisiblePositionInFlatTree SelectionModifier::ModifyMovingBackward(
TextGranularity granularity) {
VisiblePositionInFlatTree pos;
switch (granularity) {
case TextGranularity::kCharacter:
if (selection_.IsRange()) {
pos = CreateVisiblePosition(selection_.Start(), selection_.Affinity());
} else {
pos = PreviousPositionOf(ComputeVisibleExtent(selection_),
kCanSkipOverEditingBoundary);
}
break;
case TextGranularity::kWord:
pos = CreateVisiblePosition(PreviousWordPosition(
ComputeVisibleExtent(selection_).DeepEquivalent()));
break;
case TextGranularity::kSentence:
pos = CreateVisiblePosition(PreviousSentencePosition(
ComputeVisibleExtent(selection_).DeepEquivalent()));
break;
case TextGranularity::kLine: {
const VisiblePositionInFlatTree& start = StartForPlatform();
DCHECK(start.IsValid()) << start;
pos = CreateVisiblePosition(PreviousLinePosition(
start.ToPositionWithAffinity(),
LineDirectionPointForBlockDirectionNavigation(selection_.Start())));
break;
}
case TextGranularity::kParagraph:
pos = PreviousParagraphPosition(
StartForPlatform(),
LineDirectionPointForBlockDirectionNavigation(selection_.Start()));
break;
case TextGranularity::kSentenceBoundary:
pos = CreateVisiblePosition(
StartOfSentencePosition(StartForPlatform().DeepEquivalent()));
break;
case TextGranularity::kLineBoundary:
pos = LogicalStartOfLine(StartForPlatform());
break;
case TextGranularity::kParagraphBoundary:
pos = StartOfParagraph(StartForPlatform());
break;
case TextGranularity::kDocumentBoundary:
pos = StartForPlatform();
if (IsEditablePosition(pos.DeepEquivalent())) {
DCHECK(pos.IsValid()) << pos;
pos =
CreateVisiblePosition(StartOfEditableContent(pos.DeepEquivalent()));
} else {
pos = CreateVisiblePosition(StartOfDocument(pos.DeepEquivalent()));
}
break;
}
return pos;
}
static bool IsBoundary(TextGranularity granularity) {
return granularity == TextGranularity::kLineBoundary ||
granularity == TextGranularity::kParagraphBoundary ||
granularity == TextGranularity::kDocumentBoundary;
}
VisiblePositionInFlatTree SelectionModifier::ComputeModifyPosition(
SelectionModifyAlteration alter,
SelectionModifyDirection direction,
TextGranularity granularity) {
switch (direction) {
case SelectionModifyDirection::kRight:
if (alter == SelectionModifyAlteration::kMove)
return ModifyMovingRight(granularity);
return ModifyExtendingRight(granularity);
case SelectionModifyDirection::kForward:
if (alter == SelectionModifyAlteration::kExtend)
return ModifyExtendingForward(granularity);
return ModifyMovingForward(granularity);
case SelectionModifyDirection::kLeft:
if (alter == SelectionModifyAlteration::kMove)
return ModifyMovingLeft(granularity);
return ModifyExtendingLeft(granularity);
case SelectionModifyDirection::kBackward:
if (alter == SelectionModifyAlteration::kExtend)
return ModifyExtendingBackward(granularity);
return ModifyMovingBackward(granularity);
}
NOTREACHED() << static_cast<int>(direction);
return VisiblePositionInFlatTree();
}
bool SelectionModifier::Modify(SelectionModifyAlteration alter,
SelectionModifyDirection direction,
TextGranularity granularity) {
DCHECK(!GetFrame().GetDocument()->NeedsLayoutTreeUpdate());
DocumentLifecycle::DisallowTransitionScope disallow_transition(
GetFrame().GetDocument()->Lifecycle());
selection_ = PrepareToModifySelection(alter, direction);
if (selection_.IsNone())
return false;
bool was_range = selection_.IsRange();
VisiblePositionInFlatTree original_start_position = selection_.VisibleStart();
VisiblePositionInFlatTree position =
ComputeModifyPosition(alter, direction, granularity);
if (position.IsNull())
return false;
if (IsSpatialNavigationEnabled(&GetFrame())) {
if (!was_range && alter == SelectionModifyAlteration::kMove &&
position.DeepEquivalent() == original_start_position.DeepEquivalent())
return false;
}
// Some of the above operations set an xPosForVerticalArrowNavigation.
// Setting a selection will clear it, so save it to possibly restore later.
// Note: the START position type is arbitrary because it is unused, it would
// be the requested position type if there were no
// xPosForVerticalArrowNavigation set.
LayoutUnit x =
LineDirectionPointForBlockDirectionNavigation(selection_.Start());
switch (alter) {
case SelectionModifyAlteration::kMove:
current_selection_ = SelectionInFlatTree::Builder()
.Collapse(position.ToPositionWithAffinity())
.Build();
break;
case SelectionModifyAlteration::kExtend:
if (!selection_.IsCaret() &&
(granularity == TextGranularity::kWord ||
granularity == TextGranularity::kParagraph ||
granularity == TextGranularity::kLine) &&
!GetFrame()
.GetEditor()
.Behavior()
.ShouldExtendSelectionByWordOrLineAcrossCaret()) {
// Don't let the selection go across the base position directly. Needed
// to match mac behavior when, for instance, word-selecting backwards
// starting with the caret in the middle of a word and then
// word-selecting forward, leaving the caret in the same place where it
// was, instead of directly selecting to the end of the word.
const VisibleSelectionInFlatTree& new_selection =
CreateVisibleSelection(
SelectionInFlatTree::Builder(selection_.AsSelection())
.Extend(position.DeepEquivalent())
.Build());
if (selection_.IsBaseFirst() != new_selection.IsBaseFirst())
position = selection_.VisibleBase();
}
// Standard Mac behavior when extending to a boundary is grow the
// selection rather than leaving the base in place and moving the
// extent. Matches NSTextView.
if (!GetFrame()
.GetEditor()
.Behavior()
.ShouldAlwaysGrowSelectionWhenExtendingToBoundary() ||
selection_.IsCaret() || !IsBoundary(granularity)) {
current_selection_ = SelectionInFlatTree::Builder()
.Collapse(selection_.Base())
.Extend(position.DeepEquivalent())
.Build();
} else {
TextDirection text_direction = DirectionOfEnclosingBlock();
if (direction == SelectionModifyDirection::kForward ||
(text_direction == TextDirection::kLtr &&
direction == SelectionModifyDirection::kRight) ||
(text_direction == TextDirection::kRtl &&
direction == SelectionModifyDirection::kLeft)) {
current_selection_ =
SelectionInFlatTree::Builder()
.Collapse(selection_.IsBaseFirst()
? selection_.Base()
: position.DeepEquivalent())
.Extend(selection_.IsBaseFirst() ? position.DeepEquivalent()
: selection_.Extent())
.Build();
} else {
current_selection_ =
SelectionInFlatTree::Builder()
.Collapse(selection_.IsBaseFirst() ? position.DeepEquivalent()
: selection_.Base())
.Extend(selection_.IsBaseFirst() ? selection_.Extent()
: position.DeepEquivalent())
.Build();
}
}
break;
}
if (granularity == TextGranularity::kLine ||
granularity == TextGranularity::kParagraph)
x_pos_for_vertical_arrow_navigation_ = x;
return true;
}
// TODO(yosin): Maybe baseline would be better?
static bool AbsoluteCaretY(const PositionInFlatTreeWithAffinity& c, int& y) {
IntRect rect = AbsoluteCaretBoundsOf(c);
if (rect.IsEmpty())
return false;
y = rect.Y() + rect.Height() / 2;
return true;
}
bool SelectionModifier::ModifyWithPageGranularity(
SelectionModifyAlteration alter,
unsigned vertical_distance,
SelectionModifyVerticalDirection direction) {
if (!vertical_distance)
return false;
DCHECK(!GetFrame().GetDocument()->NeedsLayoutTreeUpdate());
DocumentLifecycle::DisallowTransitionScope disallow_transition(
GetFrame().GetDocument()->Lifecycle());
selection_ = PrepareToModifySelection(
alter, direction == SelectionModifyVerticalDirection::kUp
? SelectionModifyDirection::kBackward
: SelectionModifyDirection::kForward);
VisiblePositionInFlatTree pos;
LayoutUnit x_pos;
switch (alter) {
case SelectionModifyAlteration::kMove:
pos = CreateVisiblePosition(
direction == SelectionModifyVerticalDirection::kUp
? selection_.Start()
: selection_.End(),
selection_.Affinity());
x_pos = LineDirectionPointForBlockDirectionNavigation(
direction == SelectionModifyVerticalDirection::kUp
? selection_.Start()
: selection_.End());
break;
case SelectionModifyAlteration::kExtend:
pos = ComputeVisibleExtent(selection_);
x_pos =
LineDirectionPointForBlockDirectionNavigation(selection_.Extent());
break;
}
int start_y;
DCHECK(pos.IsValid()) << pos;
if (!AbsoluteCaretY(pos.ToPositionWithAffinity(), start_y))
return false;
if (direction == SelectionModifyVerticalDirection::kUp)
start_y = -start_y;
int last_y = start_y;
VisiblePositionInFlatTree result;
VisiblePositionInFlatTree next;
unsigned iteration_count = 0;
for (VisiblePositionInFlatTree p = pos;
iteration_count < kMaxIterationForPageGranularityMovement; p = next) {
++iteration_count;
if (direction == SelectionModifyVerticalDirection::kUp) {
next = CreateVisiblePosition(
PreviousLinePosition(p.ToPositionWithAffinity(), x_pos));
} else {
next = CreateVisiblePosition(
NextLinePosition(p.ToPositionWithAffinity(), x_pos));
}
if (next.IsNull() || next.DeepEquivalent() == p.DeepEquivalent())
break;
int next_y;
DCHECK(next.IsValid()) << next;
if (!AbsoluteCaretY(next.ToPositionWithAffinity(), next_y))
break;
if (direction == SelectionModifyVerticalDirection::kUp)
next_y = -next_y;
if (next_y - start_y > static_cast<int>(vertical_distance))
break;
if (next_y >= last_y) {
last_y = next_y;
result = next;
}
}
if (result.IsNull())
return false;
switch (alter) {
case SelectionModifyAlteration::kMove:
current_selection_ =
SelectionInFlatTree::Builder()
.Collapse(result.ToPositionWithAffinity())
.SetAffinity(direction == SelectionModifyVerticalDirection::kUp
? TextAffinity::kUpstream
: TextAffinity::kDownstream)
.Build();
break;
case SelectionModifyAlteration::kExtend: {
current_selection_ = SelectionInFlatTree::Builder()
.Collapse(selection_.Base())
.Extend(result.DeepEquivalent())
.Build();
break;
}
}
return true;
}
// Abs x/y position of the caret ignoring transforms.
// TODO(yosin) navigation with transforms should be smarter.
static LayoutUnit LineDirectionPointForBlockDirectionNavigationOf(
const VisiblePositionInFlatTree& visible_position) {
if (visible_position.IsNull())
return LayoutUnit();
const LocalCaretRect& caret_rect =
LocalCaretRectOfPosition(visible_position.ToPositionWithAffinity());
if (caret_rect.IsEmpty())
return LayoutUnit();
// This ignores transforms on purpose, for now. Vertical navigation is done
// without consulting transforms, so that 'up' in transformed text is 'up'
// relative to the text, not absolute 'up'.
PhysicalOffset caret_point =
UNLIKELY(caret_rect.layout_object->HasFlippedBlocksWritingMode())
? caret_rect.rect.MaxXMinYCorner()
: caret_rect.rect.MinXMinYCorner();
caret_point = caret_rect.layout_object->LocalToAbsolutePoint(
caret_point, kIgnoreTransforms);
return caret_rect.layout_object->IsHorizontalWritingMode() ? caret_point.left
: caret_point.top;
}
LayoutUnit SelectionModifier::LineDirectionPointForBlockDirectionNavigation(
const PositionInFlatTree& pos) {
LayoutUnit x;
if (selection_.IsNone())
return x;
if (x_pos_for_vertical_arrow_navigation_ ==
NoXPosForVerticalArrowNavigation()) {
VisiblePositionInFlatTree visible_position =
CreateVisiblePosition(pos, selection_.Affinity());
// VisiblePositionInFlatTree creation can fail here if a node containing the
// selection becomes visibility:hidden after the selection is created and
// before this function is called.
x = LineDirectionPointForBlockDirectionNavigationOf(visible_position);
x_pos_for_vertical_arrow_navigation_ = x;
} else {
x = x_pos_for_vertical_arrow_navigation_;
}
return x;
}
} // namespace blink