| // 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 "config.h" |
| #include "core/editing/GranularityStrategy.h" |
| |
| #include "core/editing/EditingUtilities.h" |
| #include "core/editing/FrameSelection.h" |
| |
| namespace blink { |
| |
| enum class BoundAdjust {CurrentPosIfOnBound, NextBoundIfOnBound}; |
| enum class SearchDirection {SearchBackwards, SearchForward}; |
| |
| // We use the bottom-left corner of the caret 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 absoluteCaretBoundsOf(vp).minXMaxYCorner(); |
| } |
| |
| // Order is specified using the same contract as comparePositions. |
| static bool arePositionsInSpecifiedOrder( |
| const VisiblePosition& vp1, |
| const VisiblePosition& vp2, |
| int specifiedOrder) |
| { |
| int positionOrder = comparePositions(vp1, vp2); |
| if (specifiedOrder == 0) |
| return positionOrder == 0; |
| return specifiedOrder > 0 ? positionOrder > 0 : positionOrder < 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 wordBoundAdjust) |
| { |
| bool nextBoundIfOnBound = wordBoundAdjust == BoundAdjust::NextBoundIfOnBound; |
| if (direction == SearchDirection::SearchForward) { |
| EWordSide wordSide = nextBoundIfOnBound ? RightWordIfOnBoundary : LeftWordIfOnBoundary; |
| return endOfWord(pos, wordSide); |
| } |
| EWordSide wordSide = nextBoundIfOnBound ? LeftWordIfOnBoundary : RightWordIfOnBoundary; |
| return startOfWord(pos, wordSide); |
| } |
| |
| GranularityStrategy::GranularityStrategy() { } |
| |
| GranularityStrategy::~GranularityStrategy() { } |
| |
| CharacterGranularityStrategy::CharacterGranularityStrategy() { } |
| |
| CharacterGranularityStrategy::~CharacterGranularityStrategy() { } |
| |
| SelectionStrategy CharacterGranularityStrategy::GetType() const |
| { |
| return SelectionStrategy::Character; |
| } |
| |
| void CharacterGranularityStrategy::Clear() { } |
| |
| VisibleSelection CharacterGranularityStrategy::updateExtent(const IntPoint& extentPoint, LocalFrame* frame) |
| { |
| const VisiblePosition& extentPosition = visiblePositionForContentsPoint(extentPoint, frame); |
| const VisibleSelection& selection = frame->selection().selection(); |
| if (selection.visibleBase().deepEquivalent() == extentPosition.deepEquivalent()) |
| return selection; |
| return VisibleSelection(selection.visibleBase(), extentPosition); |
| } |
| |
| DirectionGranularityStrategy::DirectionGranularityStrategy() |
| : m_state(StrategyState::Cleared) |
| , m_granularity(CharacterGranularity) |
| , m_offset(0) { } |
| |
| DirectionGranularityStrategy::~DirectionGranularityStrategy() { } |
| |
| SelectionStrategy DirectionGranularityStrategy::GetType() const |
| { |
| return SelectionStrategy::Direction; |
| } |
| |
| void DirectionGranularityStrategy::Clear() |
| { |
| m_state = StrategyState::Cleared; |
| m_granularity = CharacterGranularity; |
| m_offset = 0; |
| m_diffExtentPointFromExtentPosition = IntSize(); |
| } |
| |
| VisibleSelection DirectionGranularityStrategy::updateExtent(const IntPoint& extentPoint, LocalFrame* frame) |
| { |
| const VisibleSelection& selection = frame->selection().selection(); |
| |
| if (m_state == StrategyState::Cleared) |
| m_state = StrategyState::Expanding; |
| |
| VisiblePosition oldOffsetExtentPosition = selection.visibleExtent(); |
| IntPoint oldExtentLocation = positionLocation(oldOffsetExtentPosition); |
| |
| IntPoint oldOffsetExtentPoint = oldExtentLocation + m_diffExtentPointFromExtentPosition; |
| IntPoint oldExtentPoint = IntPoint(oldOffsetExtentPoint.x() - m_offset, oldOffsetExtentPoint.y()); |
| |
| // Apply the offset. |
| IntPoint newOffsetExtentPoint = extentPoint; |
| int dx = extentPoint.x() - oldExtentPoint.x(); |
| if (m_offset != 0) { |
| if (m_offset > 0 && dx > 0) |
| m_offset = std::max(0, m_offset - dx); |
| else if (m_offset < 0 && dx < 0) |
| m_offset = std::min(0, m_offset - dx); |
| newOffsetExtentPoint.move(m_offset, 0); |
| } |
| |
| VisiblePosition newOffsetExtentPosition = visiblePositionForContentsPoint(newOffsetExtentPoint, frame); |
| IntPoint newOffsetLocation = positionLocation(newOffsetExtentPosition); |
| |
| // 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 verticalChange = newOffsetLocation.y() != oldExtentLocation.y(); |
| if (verticalChange) { |
| m_offset = 0; |
| m_granularity = CharacterGranularity; |
| newOffsetExtentPoint = extentPoint; |
| newOffsetExtentPosition = visiblePositionForContentsPoint(extentPoint, frame); |
| } |
| |
| const VisiblePosition base = selection.visibleBase(); |
| |
| // Do not allow empty selection. |
| if (newOffsetExtentPosition.deepEquivalent() == base.deepEquivalent()) |
| return selection; |
| |
| // 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 (verticalChange && inSameLine(newOffsetExtentPosition, oldOffsetExtentPosition)) |
| return VisibleSelection(selection.visibleBase(), newOffsetExtentPosition); |
| |
| int oldExtentBaseOrder = selection.isBaseFirst() ? 1 : -1; |
| |
| int newExtentBaseOrder; |
| bool thisMoveShrunkSelection; |
| if (newOffsetExtentPosition.deepEquivalent() == oldOffsetExtentPosition.deepEquivalent()) { |
| if (m_granularity == CharacterGranularity) |
| return selection; |
| |
| // 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). |
| thisMoveShrunkSelection = false; |
| newExtentBaseOrder = oldExtentBaseOrder; |
| } else { |
| bool selectionExpanded = arePositionsInSpecifiedOrder(newOffsetExtentPosition, oldOffsetExtentPosition, oldExtentBaseOrder); |
| bool extentBaseOrderSwitched = selectionExpanded ? false : !arePositionsInSpecifiedOrder(newOffsetExtentPosition, base, oldExtentBaseOrder); |
| newExtentBaseOrder = extentBaseOrderSwitched ? -oldExtentBaseOrder : oldExtentBaseOrder; |
| |
| // Determine the word boundary, i.e. the boundary extending beyond which |
| // should change the granularity to WordGranularity. |
| VisiblePosition wordBoundary; |
| if (extentBaseOrderSwitched) { |
| // 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. |
| wordBoundary = nextWordBound( |
| base, |
| newExtentBaseOrder > 0 ? SearchDirection::SearchForward : SearchDirection::SearchBackwards, |
| BoundAdjust::NextBoundIfOnBound); |
| m_granularity = CharacterGranularity; |
| } 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. |
| wordBoundary = nextWordBound( |
| oldOffsetExtentPosition, |
| oldExtentBaseOrder > 0 ? SearchDirection::SearchForward : SearchDirection::SearchBackwards, |
| m_state == StrategyState::Shrinking ? BoundAdjust::NextBoundIfOnBound : BoundAdjust::CurrentPosIfOnBound); |
| } |
| |
| bool expandedBeyondWordBoundary; |
| if (selectionExpanded) |
| expandedBeyondWordBoundary = arePositionsInSpecifiedOrder(newOffsetExtentPosition, wordBoundary, newExtentBaseOrder); |
| else if (extentBaseOrderSwitched) |
| expandedBeyondWordBoundary = arePositionsInSpecifiedOrder(newOffsetExtentPosition, wordBoundary, newExtentBaseOrder); |
| else |
| expandedBeyondWordBoundary = false; |
| |
| // The selection is shrunk if the extent changes position to be closer to |
| // the base, and the extent/base order wasn't switched. |
| thisMoveShrunkSelection = !extentBaseOrderSwitched && !selectionExpanded; |
| |
| if (expandedBeyondWordBoundary) |
| m_granularity = WordGranularity; |
| else if (thisMoveShrunkSelection) |
| m_granularity = CharacterGranularity; |
| } |
| |
| VisiblePosition newSelectionExtent = newOffsetExtentPosition; |
| if (m_granularity == WordGranularity) { |
| // 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 boundBeforeExtent = nextWordBound(newOffsetExtentPosition, SearchDirection::SearchBackwards, BoundAdjust::CurrentPosIfOnBound); |
| VisiblePosition boundAfterExtent = nextWordBound(newOffsetExtentPosition, SearchDirection::SearchForward, BoundAdjust::CurrentPosIfOnBound); |
| int xMiddleBetweenBounds = (positionLocation(boundAfterExtent).x() + positionLocation(boundBeforeExtent).x()) / 2; |
| bool offsetExtentBeforeMiddle = newOffsetExtentPoint.x() < xMiddleBetweenBounds; |
| newSelectionExtent = offsetExtentBeforeMiddle ? boundBeforeExtent : boundAfterExtent; |
| // Update the offset if selection expanded in word granularity. |
| if (newSelectionExtent.deepEquivalent() != selection.visibleExtent().deepEquivalent() |
| && ((newExtentBaseOrder > 0 && !offsetExtentBeforeMiddle) || (newExtentBaseOrder < 0 && offsetExtentBeforeMiddle))) { |
| m_offset = positionLocation(newSelectionExtent).x() - extentPoint.x(); |
| } |
| } |
| |
| // Only update the state if the selection actually changed as a result of |
| // this move. |
| if (newSelectionExtent.deepEquivalent() != selection.visibleExtent().deepEquivalent()) |
| m_state = thisMoveShrunkSelection ? StrategyState::Shrinking : StrategyState::Expanding; |
| |
| m_diffExtentPointFromExtentPosition = extentPoint + IntSize(m_offset, 0) - positionLocation(newSelectionExtent); |
| VisibleSelection newSelection = selection; |
| newSelection.setExtent(newSelectionExtent); |
| return newSelection; |
| } |
| |
| } // namespace blink |