blob: 0acec8634c1526d115b87d95d23c0f8bbcef4d07 [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "third_party/blink/renderer/core/editing/granularity_strategy.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/editing/frame_selection.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_selection.h"
#include "third_party/blink/renderer/core/editing/visible_units.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
namespace blink {
enum class BoundAdjust { kCurrentPosIfOnBound, kNextBoundIfOnBound };
enum class SearchDirection { kSearchBackwards, kSearchForward };
// We use the bottom-left corner of the selection rect to represent the
// location of a VisiblePosition. This way locations corresponding to
// VisiblePositions on the same line will all have the same y coordinate
// unless the text is transformed.
static IntPoint PositionLocation(const VisiblePosition& vp) {
return AbsoluteSelectionBoundsOf(vp).MinXMaxYCorner();
}
// Order is specified using the same contract as comparePositions.
static bool ArePositionsInSpecifiedOrder(const VisiblePosition& vp1,
const VisiblePosition& vp2,
int specified_order) {
int position_order = ComparePositions(vp1, vp2);
if (specified_order == 0)
return position_order == 0;
return specified_order > 0 ? position_order > 0 : position_order < 0;
}
// Returns the next word boundary starting from |pos|. |direction| specifies
// the direction in which to search for the next bound. nextIfOnBound
// controls whether |pos| or the next boundary is returned when |pos| is
// located exactly on word boundary.
static VisiblePosition NextWordBound(const VisiblePosition& pos,
SearchDirection direction,
BoundAdjust word_bound_adjust) {
bool next_bound_if_on_bound =
word_bound_adjust == BoundAdjust::kNextBoundIfOnBound;
if (direction == SearchDirection::kSearchForward) {
EWordSide word_side = next_bound_if_on_bound ? kNextWordIfOnBoundary
: kPreviousWordIfOnBoundary;
return EndOfWord(pos, word_side);
}
EWordSide word_side = next_bound_if_on_bound ? kPreviousWordIfOnBoundary
: kNextWordIfOnBoundary;
return StartOfWord(pos, word_side);
}
GranularityStrategy::GranularityStrategy() = default;
GranularityStrategy::~GranularityStrategy() = default;
CharacterGranularityStrategy::CharacterGranularityStrategy() = default;
CharacterGranularityStrategy::~CharacterGranularityStrategy() = default;
SelectionStrategy CharacterGranularityStrategy::GetType() const {
return SelectionStrategy::kCharacter;
}
void CharacterGranularityStrategy::Clear() {}
SelectionInDOMTree CharacterGranularityStrategy::UpdateExtent(
const IntPoint& extent_point,
LocalFrame* frame) {
const VisiblePosition& extent_position =
VisiblePositionForContentsPoint(extent_point, frame);
const VisibleSelection& selection =
frame->Selection().ComputeVisibleSelectionInDOMTree();
if (extent_position.IsNull() || selection.VisibleBase().DeepEquivalent() ==
extent_position.DeepEquivalent())
return selection.AsSelection();
return SelectionInDOMTree::Builder()
.Collapse(selection.Base())
.Extend(extent_position.DeepEquivalent())
.SetAffinity(selection.Affinity())
.Build();
}
DirectionGranularityStrategy::DirectionGranularityStrategy()
: state_(StrategyState::kCleared),
granularity_(TextGranularity::kCharacter),
offset_(0) {}
DirectionGranularityStrategy::~DirectionGranularityStrategy() = default;
SelectionStrategy DirectionGranularityStrategy::GetType() const {
return SelectionStrategy::kDirection;
}
void DirectionGranularityStrategy::Clear() {
state_ = StrategyState::kCleared;
granularity_ = TextGranularity::kCharacter;
offset_ = 0;
diff_extent_point_from_extent_position_ = IntSize();
}
SelectionInDOMTree DirectionGranularityStrategy::UpdateExtent(
const IntPoint& extent_point,
LocalFrame* frame) {
const VisibleSelection& selection =
frame->Selection().ComputeVisibleSelectionInDOMTree();
if (state_ == StrategyState::kCleared)
state_ = StrategyState::kExpanding;
const VisiblePosition& old_offset_extent_position = selection.VisibleExtent();
IntPoint old_extent_location = PositionLocation(old_offset_extent_position);
IntPoint old_offset_extent_point =
old_extent_location + diff_extent_point_from_extent_position_;
IntPoint old_extent_point = IntPoint(old_offset_extent_point.X() - offset_,
old_offset_extent_point.Y());
// Apply the offset.
IntPoint new_offset_extent_point = extent_point;
int dx = extent_point.X() - old_extent_point.X();
if (offset_ != 0) {
if (offset_ > 0 && dx > 0)
offset_ = std::max(0, offset_ - dx);
else if (offset_ < 0 && dx < 0)
offset_ = std::min(0, offset_ - dx);
new_offset_extent_point.Move(offset_, 0);
}
VisiblePosition new_offset_extent_position =
VisiblePositionForContentsPoint(new_offset_extent_point, frame);
if (new_offset_extent_position.IsNull())
return selection.AsSelection();
IntPoint new_offset_location = PositionLocation(new_offset_extent_position);
// Reset the offset in case of a vertical change in the location (could be
// due to a line change or due to an unusual layout, e.g. rotated text).
bool vertical_change = new_offset_location.Y() != old_extent_location.Y();
if (vertical_change) {
offset_ = 0;
granularity_ = TextGranularity::kCharacter;
new_offset_extent_point = extent_point;
new_offset_extent_position =
VisiblePositionForContentsPoint(extent_point, frame);
}
const VisiblePosition base = selection.VisibleBase();
// Do not allow empty selection.
if (new_offset_extent_position.DeepEquivalent() == base.DeepEquivalent())
return selection.AsSelection();
// The direction granularity strategy, particularly the "offset" feature
// doesn't work with non-horizontal text (e.g. when the text is rotated).
// So revert to the behavior equivalent to the character granularity
// strategy if we detect that the text's baseline coordinate changed
// without a line change.
if (vertical_change &&
InSameLine(new_offset_extent_position, old_offset_extent_position)) {
return SelectionInDOMTree::Builder()
.Collapse(selection.Base())
.Extend(new_offset_extent_position.DeepEquivalent())
.SetAffinity(selection.Affinity())
.Build();
}
int old_extent_base_order = selection.IsBaseFirst() ? 1 : -1;
int new_extent_base_order;
bool this_move_shrunk_selection;
if (new_offset_extent_position.DeepEquivalent() ==
old_offset_extent_position.DeepEquivalent()) {
if (granularity_ == TextGranularity::kCharacter)
return selection.AsSelection();
// If we are in Word granularity, we cannot exit here, since we may pass
// the middle of the word without changing the position (in which case
// the selection needs to expand).
this_move_shrunk_selection = false;
new_extent_base_order = old_extent_base_order;
} else {
bool selection_expanded = ArePositionsInSpecifiedOrder(
new_offset_extent_position, old_offset_extent_position,
old_extent_base_order);
bool extent_base_order_switched =
selection_expanded
? false
: !ArePositionsInSpecifiedOrder(new_offset_extent_position, base,
old_extent_base_order);
new_extent_base_order = extent_base_order_switched ? -old_extent_base_order
: old_extent_base_order;
// Determine the word boundary, i.e. the boundary extending beyond which
// should change the granularity to WordGranularity.
VisiblePosition word_boundary;
if (extent_base_order_switched) {
// Special case.
// If the extent-base order was switched, then the selection is now
// expanding in a different direction than before. Therefore we
// calculate the word boundary in this new direction and based on
// the |base| position.
word_boundary = NextWordBound(base,
new_extent_base_order > 0
? SearchDirection::kSearchForward
: SearchDirection::kSearchBackwards,
BoundAdjust::kNextBoundIfOnBound);
granularity_ = TextGranularity::kCharacter;
} else {
// Calculate the word boundary based on |oldExtentWithGranularity|.
// If selection was shrunk in the last update and the extent is now
// exactly on the word boundary - we need to take the next bound as
// the bound of the current word.
word_boundary = NextWordBound(old_offset_extent_position,
old_extent_base_order > 0
? SearchDirection::kSearchForward
: SearchDirection::kSearchBackwards,
state_ == StrategyState::kShrinking
? BoundAdjust::kNextBoundIfOnBound
: BoundAdjust::kCurrentPosIfOnBound);
}
bool expanded_beyond_word_boundary;
if (selection_expanded)
expanded_beyond_word_boundary = ArePositionsInSpecifiedOrder(
new_offset_extent_position, word_boundary, new_extent_base_order);
else if (extent_base_order_switched)
expanded_beyond_word_boundary = ArePositionsInSpecifiedOrder(
new_offset_extent_position, word_boundary, new_extent_base_order);
else
expanded_beyond_word_boundary = false;
// The selection is shrunk if the extent changes position to be closer to
// the base, and the extent/base order wasn't switched.
this_move_shrunk_selection =
!extent_base_order_switched && !selection_expanded;
if (expanded_beyond_word_boundary)
granularity_ = TextGranularity::kWord;
else if (this_move_shrunk_selection)
granularity_ = TextGranularity::kCharacter;
}
VisiblePosition new_selection_extent = new_offset_extent_position;
if (granularity_ == TextGranularity::kWord) {
// Determine the bounds of the word where the extent is located.
// Set the selection extent to one of the two bounds depending on
// whether the extent is passed the middle of the word.
VisiblePosition bound_before_extent = NextWordBound(
new_offset_extent_position, SearchDirection::kSearchBackwards,
BoundAdjust::kCurrentPosIfOnBound);
VisiblePosition bound_after_extent = NextWordBound(
new_offset_extent_position, SearchDirection::kSearchForward,
BoundAdjust::kCurrentPosIfOnBound);
int x_middle_between_bounds = (PositionLocation(bound_after_extent).X() +
PositionLocation(bound_before_extent).X()) /
2;
bool offset_extent_before_middle =
new_offset_extent_point.X() < x_middle_between_bounds;
new_selection_extent =
offset_extent_before_middle ? bound_before_extent : bound_after_extent;
// Update the offset if selection expanded in word granularity.
if (new_selection_extent.DeepEquivalent() !=
old_offset_extent_position.DeepEquivalent() &&
((new_extent_base_order > 0 && !offset_extent_before_middle) ||
(new_extent_base_order < 0 && offset_extent_before_middle))) {
offset_ = PositionLocation(new_selection_extent).X() - extent_point.X();
}
}
// Only update the state if the selection actually changed as a result of
// this move.
if (new_selection_extent.DeepEquivalent() !=
old_offset_extent_position.DeepEquivalent())
state_ = this_move_shrunk_selection ? StrategyState::kShrinking
: StrategyState::kExpanding;
diff_extent_point_from_extent_position_ =
extent_point + IntSize(offset_, 0) -
PositionLocation(new_selection_extent);
return SelectionInDOMTree::Builder(selection.AsSelection())
.Collapse(selection.Base())
.Extend(new_selection_extent.DeepEquivalent())
.Build();
}
} // namespace blink