blob: 1b2bf5a5da9f324083ad08f03007d58f5227bb55 [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/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/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"
#include "third_party/blink/renderer/core/paint/ng/ng_paint_fragment.h"
namespace blink {
namespace {
VisiblePosition LeftBoundaryOfLine(const VisiblePosition& c,
TextDirection direction) {
DCHECK(c.IsValid()) << c;
return direction == TextDirection::kLtr ? LogicalStartOfLine(c)
: LogicalEndOfLine(c);
}
VisiblePosition RightBoundaryOfLine(const VisiblePosition& c,
TextDirection direction) {
DCHECK(c.IsValid()) << c;
return direction == TextDirection::kLtr ? LogicalEndOfLine(c)
: LogicalStartOfLine(c);
}
VisiblePosition PreviousParagraphPosition(
const VisiblePosition& passed_position,
LayoutUnit x_point) {
DCHECK(passed_position.IsValid()) << passed_position;
VisiblePosition position = passed_position;
do {
const VisiblePosition& new_position =
PreviousLinePosition(position, x_point);
if (new_position.IsNull() ||
new_position.DeepEquivalent() == position.DeepEquivalent())
break;
position = new_position;
} while (InSameParagraph(passed_position, position));
return position;
}
VisiblePosition NextParagraphPosition(const VisiblePosition& passed_position,
LayoutUnit x_point) {
DCHECK(passed_position.IsValid()) << passed_position;
VisiblePosition position = passed_position;
do {
const VisiblePosition& new_position = NextLinePosition(position, x_point);
if (new_position.IsNull() ||
new_position.DeepEquivalent() == position.DeepEquivalent())
break;
position = new_position;
} while (InSameParagraph(passed_position, position));
return position;
}
} // namespace
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_(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(current_selection_);
}
static VisiblePosition ComputeVisibleExtent(
const VisibleSelection& visible_selection) {
return CreateVisiblePosition(visible_selection.Extent(),
visible_selection.Affinity());
}
TextDirection SelectionModifier::DirectionOfEnclosingBlock() const {
const Position& selection_extent = selection_.Extent();
// TODO(editing-dev): Check for Position::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 VisiblePosition& position) {
if (position.IsNull())
return base::nullopt;
const PositionWithAffinity adjusted = ComputeInlineAdjustedPosition(position);
if (adjusted.IsNull())
return base::nullopt;
if (NGInlineFormattingContextOf(adjusted.GetPosition())) {
if (const NGPaintFragment* fragment =
ComputeNGCaretPosition(adjusted).fragment)
return fragment->PhysicalFragment().ResolvedDirection();
return base::nullopt;
}
if (const InlineBox* box =
ComputeInlineBoxPositionForInlineAdjustedPosition(adjusted)
.inline_box)
return box->Direction();
return base::nullopt;
}
TextDirection DirectionOf(const VisibleSelection& visible_selection) {
base::Optional<TextDirection> maybe_start_direction =
DirectionAt(visible_selection.VisibleStart());
base::Optional<TextDirection> maybe_end_direction =
DirectionAt(visible_selection.VisibleEnd());
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_);
}
static bool IsBaseStart(const VisibleSelection& 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 |VisibleSelection| from start and end position of
// current_selection_'s |VisibleSelection| 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 |visibleSelection::validate()| for
// range selection.
VisibleSelection SelectionModifier::PrepareToModifySelection(
SelectionModifyAlteration alter,
SelectionModifyDirection direction) const {
const VisibleSelection& visible_selection =
CreateVisibleSelection(current_selection_);
if (alter != SelectionModifyAlteration::kExtend)
return visible_selection;
if (visible_selection.IsNone())
return visible_selection;
const EphemeralRange& range = visible_selection.AsSelection().ComputeRange();
if (range.IsCollapsed())
return visible_selection;
SelectionInDOMTree::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());
}
VisiblePosition SelectionModifier::PositionForPlatform(
bool is_get_start) const {
Settings* settings = GetFrame().GetSettings();
if (settings && settings->GetEditingBehaviorType() == kEditingMacBehavior)
return is_get_start ? selection_.VisibleStart() : selection_.VisibleEnd();
// Linux and Windows always extend selections from the extent endpoint.
// FIXME: VisibleSelection 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 m_sel.extent().
return selection_.IsBaseFirst() ? selection_.VisibleEnd()
: selection_.VisibleStart();
}
VisiblePosition SelectionModifier::StartForPlatform() const {
return PositionForPlatform(true);
}
VisiblePosition SelectionModifier::EndForPlatform() const {
return PositionForPlatform(false);
}
Position SelectionModifier::NextWordPositionForPlatform(
const Position& original_position) {
// Next word position can't be upstream.
const Position position_after_current_word =
NextWordPosition(original_position).GetPosition();
if (!GetFrame().GetEditor().Behavior().ShouldSkipSpaceWhenMovingRight())
return position_after_current_word;
return SkipWhitespace(position_after_current_word);
}
static VisiblePosition AdjustForwardPositionForUserSelectAll(
const VisiblePosition& position) {
Node* const root_user_select_all = EditingStrategy::RootUserSelectAllForNode(
position.DeepEquivalent().AnchorNode());
if (!root_user_select_all)
return position;
return CreateVisiblePosition(MostForwardCaretPosition(
Position::AfterNode(*root_user_select_all), kCanCrossEditingBoundary));
}
static VisiblePosition AdjustBackwardPositionForUserSelectAll(
const VisiblePosition& position) {
Node* const root_user_select_all = EditingStrategy::RootUserSelectAllForNode(
position.DeepEquivalent().AnchorNode());
if (!root_user_select_all)
return position;
return CreateVisiblePosition(MostBackwardCaretPosition(
Position::BeforeNode(*root_user_select_all), kCanCrossEditingBoundary));
}
VisiblePosition 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 VisiblePosition();
}
VisiblePosition SelectionModifier::ModifyExtendingRight(
TextGranularity granularity) {
const VisiblePosition& pos = ModifyExtendingRightInternal(granularity);
if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
return AdjustForwardPositionForUserSelectAll(pos);
return AdjustBackwardPositionForUserSelectAll(pos);
}
VisiblePosition 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 NextSentencePosition(ComputeVisibleExtent(selection_));
case TextGranularity::kLine:
return NextLinePosition(
ComputeVisibleExtent(selection_),
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());
break;
case TextGranularity::kDocumentBoundary: {
const VisiblePosition& pos = EndForPlatform();
if (IsEditablePosition(pos.DeepEquivalent()))
return EndOfEditableContent(pos);
return EndOfDocument(pos);
}
}
NOTREACHED() << static_cast<int>(granularity);
return VisiblePosition();
}
VisiblePosition SelectionModifier::ModifyExtendingForward(
TextGranularity granularity) {
const VisiblePosition pos = ModifyExtendingForwardInternal(granularity);
if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
return AdjustForwardPositionForUserSelectAll(pos);
return AdjustBackwardPositionForUserSelectAll(pos);
}
VisiblePosition SelectionModifier::ModifyMovingRight(
TextGranularity granularity) {
switch (granularity) {
case TextGranularity::kCharacter:
if (!selection_.IsRange()) {
return RightPositionOf(ComputeVisibleExtent(selection_));
}
if (DirectionOfSelection() == TextDirection::kLtr)
return CreateVisiblePosition(selection_.End(), selection_.Affinity());
return CreateVisiblePosition(selection_.Start(), selection_.Affinity());
case TextGranularity::kWord: {
const bool skips_space_when_moving_right =
GetFrame().GetEditor().Behavior().ShouldSkipSpaceWhenMovingRight();
return RightWordPosition(ComputeVisibleExtent(selection_),
skips_space_when_moving_right);
}
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 VisiblePosition();
}
VisiblePosition 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 NextSentencePosition(ComputeVisibleExtent(selection_));
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 VisiblePosition& pos = EndForPlatform();
if (selection_.IsRange() && IsStartOfLine(pos))
return pos;
return NextLinePosition(
pos,
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 VisiblePosition& pos = EndForPlatform();
if (IsEditablePosition(pos.DeepEquivalent()))
return EndOfEditableContent(pos);
return EndOfDocument(pos);
}
}
NOTREACHED() << static_cast<int>(granularity);
return VisiblePosition();
}
VisiblePosition 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 VisiblePosition();
}
VisiblePosition SelectionModifier::ModifyExtendingLeft(
TextGranularity granularity) {
const VisiblePosition& pos = ModifyExtendingLeftInternal(granularity);
if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
return AdjustBackwardPositionForUserSelectAll(pos);
return AdjustForwardPositionForUserSelectAll(pos);
}
VisiblePosition 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 VisiblePosition 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 PreviousSentencePosition(ComputeVisibleExtent(selection_));
case TextGranularity::kLine:
return PreviousLinePosition(
ComputeVisibleExtent(selection_),
LineDirectionPointForBlockDirectionNavigation(selection_.Extent()));
case TextGranularity::kParagraph:
return PreviousParagraphPosition(
ComputeVisibleExtent(selection_),
LineDirectionPointForBlockDirectionNavigation(selection_.Extent()));
case TextGranularity::kSentenceBoundary:
return StartOfSentence(StartForPlatform());
case TextGranularity::kLineBoundary:
return LogicalStartOfLine(StartForPlatform());
case TextGranularity::kParagraphBoundary:
return StartOfParagraph(StartForPlatform());
case TextGranularity::kDocumentBoundary: {
const VisiblePosition pos = StartForPlatform();
if (IsEditablePosition(pos.DeepEquivalent()))
return StartOfEditableContent(pos);
return StartOfDocument(pos);
}
}
NOTREACHED() << static_cast<int>(granularity);
return VisiblePosition();
}
VisiblePosition SelectionModifier::ModifyExtendingBackward(
TextGranularity granularity) {
const VisiblePosition pos = ModifyExtendingBackwardInternal(granularity);
if (DirectionOfEnclosingBlock() == TextDirection::kLtr)
return AdjustBackwardPositionForUserSelectAll(pos);
return AdjustForwardPositionForUserSelectAll(pos);
}
VisiblePosition SelectionModifier::ModifyMovingLeft(
TextGranularity granularity) {
switch (granularity) {
case TextGranularity::kCharacter:
if (!selection_.IsRange()) {
return LeftPositionOf(ComputeVisibleExtent(selection_));
}
if (DirectionOfSelection() == TextDirection::kLtr)
return CreateVisiblePosition(selection_.Start(), selection_.Affinity());
return CreateVisiblePosition(selection_.End(), selection_.Affinity());
case TextGranularity::kWord: {
const bool skips_space_when_moving_right =
GetFrame().GetEditor().Behavior().ShouldSkipSpaceWhenMovingRight();
return LeftWordPosition(ComputeVisibleExtent(selection_),
skips_space_when_moving_right);
}
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 VisiblePosition();
}
VisiblePosition SelectionModifier::ModifyMovingBackward(
TextGranularity granularity) {
VisiblePosition 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 = PreviousSentencePosition(ComputeVisibleExtent(selection_));
break;
case TextGranularity::kLine:
pos = PreviousLinePosition(
StartForPlatform(),
LineDirectionPointForBlockDirectionNavigation(selection_.Start()));
break;
case TextGranularity::kParagraph:
pos = PreviousParagraphPosition(
StartForPlatform(),
LineDirectionPointForBlockDirectionNavigation(selection_.Start()));
break;
case TextGranularity::kSentenceBoundary:
pos = StartOfSentence(StartForPlatform());
break;
case TextGranularity::kLineBoundary:
pos = LogicalStartOfLine(StartForPlatform());
break;
case TextGranularity::kParagraphBoundary:
pos = StartOfParagraph(StartForPlatform());
break;
case TextGranularity::kDocumentBoundary:
pos = StartForPlatform();
if (IsEditablePosition(pos.DeepEquivalent()))
pos = StartOfEditableContent(pos);
else
pos = StartOfDocument(pos);
break;
}
return pos;
}
static bool IsBoundary(TextGranularity granularity) {
return granularity == TextGranularity::kLineBoundary ||
granularity == TextGranularity::kParagraphBoundary ||
granularity == TextGranularity::kDocumentBoundary;
}
VisiblePosition 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 VisiblePosition();
}
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();
VisiblePosition original_start_position = selection_.VisibleStart();
VisiblePosition 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_ =
SelectionInDOMTree::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 VisibleSelection& new_selection = CreateVisibleSelection(
SelectionInDOMTree::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_ = SelectionInDOMTree::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_ =
SelectionInDOMTree::Builder()
.Collapse(selection_.IsBaseFirst()
? selection_.Base()
: position.DeepEquivalent())
.Extend(selection_.IsBaseFirst() ? position.DeepEquivalent()
: selection_.Extent())
.Build();
} else {
current_selection_ =
SelectionInDOMTree::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 VisiblePosition& c, int& y) {
DCHECK(c.IsValid()) << c;
IntRect rect = AbsoluteCaretBoundsOf(c.ToPositionWithAffinity());
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);
VisiblePosition 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;
if (!AbsoluteCaretY(pos, start_y))
return false;
if (direction == SelectionModifyVerticalDirection::kUp)
start_y = -start_y;
int last_y = start_y;
VisiblePosition result;
VisiblePosition next;
for (VisiblePosition p = pos;; p = next) {
if (direction == SelectionModifyVerticalDirection::kUp)
next = PreviousLinePosition(p, x_pos);
else
next = NextLinePosition(p, x_pos);
if (next.IsNull() || next.DeepEquivalent() == p.DeepEquivalent())
break;
int next_y;
if (!AbsoluteCaretY(next, 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_ =
SelectionInDOMTree::Builder()
.Collapse(result.ToPositionWithAffinity())
.SetAffinity(direction == SelectionModifyVerticalDirection::kUp
? TextAffinity::kUpstream
: TextAffinity::kDownstream)
.Build();
break;
case SelectionModifyAlteration::kExtend: {
current_selection_ = SelectionInDOMTree::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 VisiblePosition& 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'.
const FloatPoint& caret_point = caret_rect.layout_object->LocalToAbsolute(
FloatPoint(caret_rect.rect.Location()));
const LayoutObject* const containing_block =
caret_rect.layout_object->ContainingBlock();
// Just use ourselves to determine the writing mode if we have no containing
// block.
const LayoutObject* const layout_object =
containing_block ? containing_block : caret_rect.layout_object;
return LayoutUnit(layout_object->IsHorizontalWritingMode() ? caret_point.X()
: caret_point.Y());
}
LayoutUnit SelectionModifier::LineDirectionPointForBlockDirectionNavigation(
const Position& pos) {
LayoutUnit x;
if (selection_.IsNone())
return x;
if (x_pos_for_vertical_arrow_navigation_ ==
NoXPosForVerticalArrowNavigation()) {
VisiblePosition visible_position =
CreateVisiblePosition(pos, selection_.Affinity());
// VisiblePosition 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