/* * Copyright (C) 2004-2020 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 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 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 "config.h" #include "FrameSelection.h" #include "AXObjectCache.h" #include "CharacterData.h" #include "ColorBlending.h" #include "DeleteSelectionCommand.h" #include "Document.h" #include "Editing.h" #include "Editor.h" #include "EditorClient.h" #include "Element.h" #include "ElementIterator.h" #include "Event.h" #include "EventNames.h" #include "FloatQuad.h" #include "FocusController.h" #include "Frame.h" #include "FrameTree.h" #include "FrameView.h" #include "GraphicsContext.h" #include "HTMLBodyElement.h" #include "HTMLFormElement.h" #include "HTMLFrameElement.h" #include "HTMLIFrameElement.h" #include "HTMLNames.h" #include "HTMLSelectElement.h" #include "HitTestRequest.h" #include "HitTestResult.h" #include "InlineRunAndOffset.h" #include "LegacyInlineTextBox.h" #include "Logging.h" #include "Page.h" #include "Range.h" #include "RenderLayer.h" #include "RenderLayerScrollableArea.h" #include "RenderText.h" #include "RenderTextControl.h" #include "RenderTheme.h" #include "RenderView.h" #include "RenderWidget.h" #include "RenderedPosition.h" #include "ScriptDisallowedScope.h" #include "Settings.h" #include "SimpleRange.h" #include "SpatialNavigation.h" #include "StyleProperties.h" #include "TypingCommand.h" #include "VisibleUnits.h" #include #include #include #if PLATFORM(IOS_FAMILY) #include "Chrome.h" #include "ChromeClient.h" #include "Color.h" #include "RenderObject.h" #include "RenderStyle.h" #include "SelectionGeometry.h" #endif namespace WebCore { using namespace HTMLNames; static inline LayoutUnit NoXPosForVerticalArrowNavigation() { return LayoutUnit::min(); } CaretBase::CaretBase(CaretVisibility visibility) : m_caretRectNeedsUpdate(true) , m_caretVisibility(visibility) { } DragCaretController::DragCaretController() : CaretBase(Visible) { } bool DragCaretController::isContentRichlyEditable() const { return isRichlyEditablePosition(m_position.deepEquivalent()); } IntRect DragCaretController::caretRectInRootViewCoordinates() const { if (!hasCaret()) return { }; if (auto* document = m_position.deepEquivalent().document()) { if (auto* documentView = document->view()) return documentView->contentsToRootView(m_position.absoluteCaretBounds()); } return { }; } IntRect DragCaretController::editableElementRectInRootViewCoordinates() const { if (!hasCaret()) return { }; RefPtr editableContainer; if (auto* formControl = enclosingTextFormControl(m_position.deepEquivalent())) editableContainer = formControl; else editableContainer = highestEditableRoot(m_position.deepEquivalent()); if (!editableContainer) return { }; auto* renderer = editableContainer->renderer(); if (!renderer) return { }; if (auto* view = editableContainer->document().view()) return view->contentsToRootView(renderer->absoluteBoundingBoxRect()); // FIXME: Wrong for elements with visible layout overflow. return { }; } static inline bool shouldAlwaysUseDirectionalSelection(Document* document) { return !document || document->editor().behavior().shouldConsiderSelectionAsDirectional(); } FrameSelection::FrameSelection(Document* document) : m_document(makeWeakPtr(document)) , m_granularity(TextGranularity::CharacterGranularity) #if ENABLE(TEXT_CARET) , m_caretBlinkTimer(*this, &FrameSelection::caretBlinkTimerFired) #endif , m_appearanceUpdateTimer(*this, &FrameSelection::appearanceUpdateTimerFired) , m_caretInsidePositionFixed(false) , m_absCaretBoundsDirty(true) , m_caretPaint(true) , m_isCaretBlinkingSuspended(false) , m_focused(document && document->frame() && document->page() && document->page()->focusController().focusedFrame() == document->frame()) , m_shouldShowBlockCursor(false) , m_pendingSelectionUpdate(false) , m_alwaysAlignCursorOnScrollWhenRevealingSelection(false) #if PLATFORM(IOS_FAMILY) , m_updateAppearanceEnabled(false) , m_caretBlinks(true) #endif { if (shouldAlwaysUseDirectionalSelection(m_document.get())) m_selection.setIsDirectional(true); bool activeAndFocused = isFocusedAndActive(); if (activeAndFocused) setSelectionFromNone(); #if USE(UIKIT_EDITING) // Caret blinking (blinks | does not blink) setCaretVisible(activeAndFocused); #else setCaretVisibility(activeAndFocused ? Visible : Hidden, ShouldUpdateAppearance::No); #endif } Element* FrameSelection::rootEditableElementOrDocumentElement() const { Element* selectionRoot = m_selection.rootEditableElement(); return selectionRoot ? selectionRoot : m_document->documentElement(); } void FrameSelection::moveTo(const VisiblePosition& position, EUserTriggered userTriggered, CursorAlignOnScroll align) { setSelection(VisibleSelection(position.deepEquivalent(), position.deepEquivalent(), position.affinity(), m_selection.isDirectional()), defaultSetSelectionOptions(userTriggered), AXTextStateChangeIntent(), align); } void FrameSelection::moveTo(const VisiblePosition& base, const VisiblePosition& extent, EUserTriggered userTriggered) { const bool selectionHasDirection = true; setSelection(VisibleSelection(base.deepEquivalent(), extent.deepEquivalent(), base.affinity(), selectionHasDirection), defaultSetSelectionOptions(userTriggered)); } void FrameSelection::moveTo(const Position& position, Affinity affinity, EUserTriggered userTriggered) { setSelection(VisibleSelection(position, affinity, m_selection.isDirectional()), defaultSetSelectionOptions(userTriggered)); } void FrameSelection::moveTo(const Position& base, const Position& extent, Affinity affinity, EUserTriggered userTriggered) { const bool selectionHasDirection = true; setSelection(VisibleSelection(base, extent, affinity, selectionHasDirection), defaultSetSelectionOptions(userTriggered)); } void FrameSelection::moveWithoutValidationTo(const Position& base, const Position& extent, bool selectionHasDirection, bool shouldSetFocus, SelectionRevealMode revealMode, const AXTextStateChangeIntent& intent) { VisibleSelection newSelection; newSelection.setWithoutValidation(base, extent); newSelection.setIsDirectional(selectionHasDirection); AXTextStateChangeIntent newIntent = intent.type == AXTextStateChangeTypeUnknown ? AXTextStateChangeIntent(AXTextStateChangeTypeSelectionMove, AXTextSelection { AXTextSelectionDirectionDiscontiguous, AXTextSelectionGranularityUnknown, false }) : intent; auto options = defaultSetSelectionOptions(); if (!shouldSetFocus) options.add(DoNotSetFocus); switch (revealMode) { case SelectionRevealMode::DoNotReveal: break; case SelectionRevealMode::Reveal: options.add(RevealSelection); break; case SelectionRevealMode::RevealUpToMainFrame: options.add(RevealSelectionUpToMainFrame); break; case SelectionRevealMode::DelegateMainFrameScroll: options.add(DelegateMainFrameScroll); break; } setSelection(newSelection, options, newIntent); } void DragCaretController::setCaretPosition(const VisiblePosition& position) { if (Node* node = m_position.deepEquivalent().deprecatedNode()) invalidateCaretRect(node); m_position = position; setCaretRectNeedsUpdate(); Document* document = nullptr; if (Node* node = m_position.deepEquivalent().deprecatedNode()) { invalidateCaretRect(node); document = &node->document(); } if (m_position.isNull() || m_position.isOrphan()) clearCaretRect(); else updateCaretRect(*document, m_position); } static void adjustEndpointsAtBidiBoundary(VisiblePosition& visibleBase, VisiblePosition& visibleExtent) { RenderedPosition base(visibleBase); RenderedPosition extent(visibleExtent); if (base.isNull() || extent.isNull() || base.isEquivalent(extent)) return; if (base.atLeftBoundaryOfBidiRun()) { if (!extent.atRightBoundaryOfBidiRun(base.bidiLevelOnRight()) && base.isEquivalent(extent.leftBoundaryOfBidiRun(base.bidiLevelOnRight()))) { visibleBase = base.positionAtLeftBoundaryOfBiDiRun(); return; } return; } if (base.atRightBoundaryOfBidiRun()) { if (!extent.atLeftBoundaryOfBidiRun(base.bidiLevelOnLeft()) && base.isEquivalent(extent.rightBoundaryOfBidiRun(base.bidiLevelOnLeft()))) { visibleBase = base.positionAtRightBoundaryOfBiDiRun(); return; } return; } if (extent.atLeftBoundaryOfBidiRun() && extent.isEquivalent(base.leftBoundaryOfBidiRun(extent.bidiLevelOnRight()))) { visibleExtent = extent.positionAtLeftBoundaryOfBiDiRun(); return; } if (extent.atRightBoundaryOfBidiRun() && extent.isEquivalent(base.rightBoundaryOfBidiRun(extent.bidiLevelOnLeft()))) { visibleExtent = extent.positionAtRightBoundaryOfBiDiRun(); return; } } void FrameSelection::setSelectionByMouseIfDifferent(const VisibleSelection& passedNewSelection, TextGranularity granularity, EndPointsAdjustmentMode endpointsAdjustmentMode) { VisibleSelection newSelection = passedNewSelection; bool isDirectional = shouldAlwaysUseDirectionalSelection(m_document.get()) || newSelection.isDirectional(); VisiblePosition base = m_originalBase.isNotNull() ? m_originalBase : newSelection.visibleBase(); VisiblePosition newBase = base; VisiblePosition extent = newSelection.visibleExtent(); VisiblePosition newExtent = extent; if (endpointsAdjustmentMode == EndPointsAdjustmentMode::AdjustAtBidiBoundary) adjustEndpointsAtBidiBoundary(newBase, newExtent); if (newBase != base || newExtent != extent) { m_originalBase = base; newSelection.setBase(newBase); newSelection.setExtent(newExtent); } else if (m_originalBase.isNotNull()) { if (m_selection.base() == newSelection.base()) newSelection.setBase(m_originalBase); m_originalBase = { }; } newSelection.setIsDirectional(isDirectional); // Adjusting base and extent will make newSelection always directional if (m_selection == newSelection || !shouldChangeSelection(newSelection)) return; AXTextStateChangeIntent intent; if (AXObjectCache::accessibilityEnabled() && newSelection.isCaret()) intent = AXTextStateChangeIntent(AXTextStateChangeTypeSelectionMove, AXTextSelection { AXTextSelectionDirectionDiscontiguous, AXTextSelectionGranularityUnknown, false }); else intent = AXTextStateChangeIntent(); setSelection(newSelection, defaultSetSelectionOptions() | FireSelectEvent, intent, AlignCursorOnScrollIfNeeded, granularity); } bool FrameSelection::setSelectionWithoutUpdatingAppearance(const VisibleSelection& newSelectionPossiblyWithoutDirection, OptionSet options, CursorAlignOnScroll align, TextGranularity granularity) { bool closeTyping = options.contains(CloseTyping); bool shouldClearTypingStyle = options.contains(ClearTypingStyle); VisibleSelection newSelection = newSelectionPossiblyWithoutDirection; if (shouldAlwaysUseDirectionalSelection(m_document.get())) newSelection.setIsDirectional(true); // : Infinite recursion at FrameSelection::setSelection // if document->frame() == m_document->frame() we can get into an infinite loop if (Document* newSelectionDocument = newSelection.base().document()) { if (RefPtr newSelectionFrame = newSelectionDocument->frame()) { if (m_document && newSelectionFrame != m_document->frame() && newSelectionDocument != m_document) { newSelectionDocument->selection().setSelection(newSelection, options, AXTextStateChangeIntent(), align, granularity); // It's possible that during the above set selection, this FrameSelection has been modified by // selectFrameElementInParentIfFullySelected, but that the selection is no longer valid since // the frame is about to be destroyed. If this is the case, clear our selection. if (newSelectionFrame->hasOneRef() && m_selection.isNoneOrOrphaned()) clear(); return false; } } } VisibleSelection oldSelection = m_selection; bool willMutateSelection = oldSelection != newSelection; if (willMutateSelection && m_document) m_document->editor().selectionWillChange(); { ScriptDisallowedScope::InMainThread scriptDisallowedScope; if (newSelection.isOrphan()) { ASSERT_NOT_REACHED(); clear(); return false; } if (!m_document || (!m_document->frame() && !newSelection.document())) { m_selection = newSelection; updateAssociatedLiveRange(); return false; } bool selectionEndpointsBelongToMultipleDocuments = newSelection.base().document() && !newSelection.document(); bool selectionIsInAnotherDocument = newSelection.document() && newSelection.document() != m_document.get(); bool selectionIsInDetachedDocument = newSelection.document() && !newSelection.document()->frame(); if (selectionEndpointsBelongToMultipleDocuments || selectionIsInAnotherDocument || selectionIsInDetachedDocument) { clear(); return false; } ASSERT(m_document->frame()); if (closeTyping) TypingCommand::closeTyping(*m_document); if (shouldClearTypingStyle) clearTypingStyle(); m_granularity = granularity; m_selection = newSelection; updateAssociatedLiveRange(); } // Selection offsets should increase when LF is inserted before the caret in InsertLineBreakCommand. See . if (HTMLTextFormControlElement* textControl = enclosingTextFormControl(newSelection.start())) textControl->selectionChanged(options.contains(FireSelectEvent)); if (!willMutateSelection) return false; setCaretRectNeedsUpdate(); if (!newSelection.isNone() && !(options & DoNotSetFocus)) { auto* oldFocusedElement = m_document->focusedElement(); setFocusedElementIfNeeded(); if (!m_document->frame()) return false; // FIXME: Should not be needed. if (m_document->focusedElement() != oldFocusedElement) m_document->updateStyleIfNeeded(); } // Always clear the x position used for vertical arrow navigation. // It will be restored by the vertical arrow navigation code if necessary. m_xPosForVerticalArrowNavigation = std::nullopt; selectFrameElementInParentIfFullySelected(); m_document->editor().respondToChangedSelection(oldSelection, options); // https://www.w3.org/TR/selection-api/#selectionchange-event // FIXME: Spec doesn't specify which task source to use. m_document->queueTaskToDispatchEvent(TaskSource::UserInteraction, Event::create(eventNames().selectionchangeEvent, Event::CanBubble::No, Event::IsCancelable::No)); return true; } void FrameSelection::setSelection(const VisibleSelection& selection, OptionSet options, AXTextStateChangeIntent intent, CursorAlignOnScroll align, TextGranularity granularity) { LOG_WITH_STREAM(Selection, stream << "FrameSelection::setSelection " << selection); auto protectedDocument = makeRefPtr(m_document.get()); if (!setSelectionWithoutUpdatingAppearance(selection, options, align, granularity)) return; if (options & RevealSelectionUpToMainFrame) m_selectionRevealMode = SelectionRevealMode::RevealUpToMainFrame; else if (options & RevealSelection) m_selectionRevealMode = SelectionRevealMode::Reveal; else if (options & DelegateMainFrameScroll) m_selectionRevealMode = SelectionRevealMode::DelegateMainFrameScroll; else m_selectionRevealMode = SelectionRevealMode::DoNotReveal; m_alwaysAlignCursorOnScrollWhenRevealingSelection = align == AlignCursorOnScrollAlways; m_selectionRevealIntent = intent; m_pendingSelectionUpdate = true; if (protectedDocument->hasPendingStyleRecalc()) return; auto frameView = protectedDocument->view(); if (frameView && frameView->layoutContext().isLayoutPending()) return; updateAndRevealSelection(intent, options.contains(SmoothScroll) ? ScrollBehavior::Smooth : ScrollBehavior::Instant, options.contains(RevealSelectionBounds) ? RevealExtentOption::DoNotRevealExtent : RevealExtentOption::RevealExtent); if (options & IsUserTriggered) { if (auto* client = protectedDocument->editor().client()) client->didEndUserTriggeredSelectionChanges(); } } static void updateSelectionByUpdatingLayoutOrStyle(Document& document) { #if ENABLE(TEXT_CARET) document.updateLayoutIgnorePendingStylesheets(); #else document.updateStyleIfNeeded(); #endif } void FrameSelection::setNeedsSelectionUpdate(RevealSelectionAfterUpdate revealMode) { m_selectionRevealIntent = AXTextStateChangeIntent(); if (revealMode == RevealSelectionAfterUpdate::Forced) m_selectionRevealMode = SelectionRevealMode::Reveal; m_pendingSelectionUpdate = true; if (RenderView* view = m_document->renderView()) view->selection().clear(); } void FrameSelection::updateAndRevealSelection(const AXTextStateChangeIntent& intent, ScrollBehavior scrollBehavior, RevealExtentOption revealExtent) { if (!m_pendingSelectionUpdate) return; m_pendingSelectionUpdate = false; updateAppearance(); if (m_selectionRevealMode != SelectionRevealMode::DoNotReveal) { ScrollAlignment alignment; if (m_document->editor().behavior().shouldCenterAlignWhenSelectionIsRevealed()) alignment = m_alwaysAlignCursorOnScrollWhenRevealingSelection ? ScrollAlignment::alignCenterAlways : ScrollAlignment::alignCenterIfNeeded; else alignment = m_alwaysAlignCursorOnScrollWhenRevealingSelection ? ScrollAlignment::alignTopAlways : ScrollAlignment::alignToEdgeIfNeeded; revealSelection(m_selectionRevealMode, alignment, revealExtent, scrollBehavior); } notifyAccessibilityForSelectionChange(intent); } void FrameSelection::updateDataDetectorsForSelection() { #if ENABLE(TELEPHONE_NUMBER_DETECTION) && !PLATFORM(IOS_FAMILY) m_document->editor().scanSelectionForTelephoneNumbers(); #endif } static bool removingNodeRemovesPosition(Node& node, const Position& position) { if (!position.anchorNode()) return false; if (position.anchorNode() == &node) return true; if (!is(node)) return false; return downcast(node).containsIncludingShadowDOM(position.anchorNode()); } void DragCaretController::nodeWillBeRemoved(Node& node) { if (!hasCaret() || !node.isConnected()) return; if (!removingNodeRemovesPosition(node, m_position.deepEquivalent())) return; if (RenderView* view = node.document().renderView()) view->selection().clear(); clear(); } void FrameSelection::nodeWillBeRemoved(Node& node) { // There can't be a selection inside a fragment, so if a fragment's node is being removed, // the selection in the document that created the fragment needs no adjustment. if (isNone() || !node.isConnected()) return; respondToNodeModification(node, removingNodeRemovesPosition(node, m_selection.base()), removingNodeRemovesPosition(node, m_selection.extent()), removingNodeRemovesPosition(node, m_selection.start()), removingNodeRemovesPosition(node, m_selection.end())); } void FrameSelection::respondToNodeModification(Node& node, bool baseRemoved, bool extentRemoved, bool startRemoved, bool endRemoved) { bool clearRenderTreeSelection = false; bool clearDOMTreeSelection = false; if (startRemoved || endRemoved) { Position start = m_selection.start(); Position end = m_selection.end(); if (startRemoved) updatePositionForNodeRemoval(start, node); if (endRemoved) updatePositionForNodeRemoval(end, node); if (start.isNotNull() && end.isNotNull()) { if (m_selection.isBaseFirst()) m_selection.setWithoutValidation(start, end); else m_selection.setWithoutValidation(end, start); } else clearDOMTreeSelection = true; clearRenderTreeSelection = true; } else if (baseRemoved || extentRemoved) { // The base and/or extent are about to be removed, but the start and end aren't. // Change the base and extent to the start and end, but don't re-validate the // selection, since doing so could move the start and end into the node // that is about to be removed. if (m_selection.isBaseFirst()) m_selection.setWithoutValidation(m_selection.start(), m_selection.end()); else m_selection.setWithoutValidation(m_selection.end(), m_selection.start()); } else if (isRange()) { if (auto range = m_selection.firstRange(); range && intersects(*range, node)) { // If we did nothing here, when this node's renderer was destroyed, the rect that it // occupied would be invalidated, but, selection gaps that change as a result of // the removal wouldn't be invalidated. // FIXME: Don't do so much unnecessary invalidation. clearRenderTreeSelection = true; } } if (clearRenderTreeSelection) { if (auto* renderView = node.document().renderView()) { renderView->selection().clear(); // Trigger a selection update so the selection will be set again. m_selectionRevealIntent = AXTextStateChangeIntent(); m_pendingSelectionUpdate = true; renderView->frameView().scheduleSelectionUpdate(); } } if (clearDOMTreeSelection) setSelection(VisibleSelection(), DoNotSetFocus); } static void updatePositionAfterAdoptingTextReplacement(Position& position, CharacterData* node, unsigned offset, unsigned oldLength, unsigned newLength) { if (!position.anchorNode() || position.anchorNode() != node || position.anchorType() != Position::PositionIsOffsetInAnchor) return; // See: http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Mutation ASSERT(position.offsetInContainerNode() >= 0); unsigned positionOffset = static_cast(position.offsetInContainerNode()); // Replacing text can be viewed as a deletion followed by insertion. if (positionOffset >= offset && positionOffset <= offset + oldLength) position.moveToOffset(offset); // Adjust the offset if the position is after the end of the deleted contents // (positionOffset > offset + oldLength) to avoid having a stale offset. if (positionOffset > offset + oldLength) position.moveToOffset(positionOffset - oldLength + newLength); ASSERT(static_cast(position.offsetInContainerNode()) <= node->length()); } void FrameSelection::textWasReplaced(CharacterData* node, unsigned offset, unsigned oldLength, unsigned newLength) { // The fragment check is a performance optimization. See http://trac.webkit.org/changeset/30062. if (isNone() || !node || !node->isConnected()) return; Position base = m_selection.base(); Position extent = m_selection.extent(); Position start = m_selection.start(); Position end = m_selection.end(); updatePositionAfterAdoptingTextReplacement(base, node, offset, oldLength, newLength); updatePositionAfterAdoptingTextReplacement(extent, node, offset, oldLength, newLength); updatePositionAfterAdoptingTextReplacement(start, node, offset, oldLength, newLength); updatePositionAfterAdoptingTextReplacement(end, node, offset, oldLength, newLength); if (base != m_selection.base() || extent != m_selection.extent() || start != m_selection.start() || end != m_selection.end()) { VisibleSelection newSelection; if (base != extent) newSelection.setWithoutValidation(base, extent); else if (m_selection.isDirectional() && !m_selection.isBaseFirst()) newSelection.setWithoutValidation(end, start); else newSelection.setWithoutValidation(start, end); setSelection(newSelection, DoNotSetFocus); } } TextDirection FrameSelection::directionOfEnclosingBlock() { return WebCore::directionOfEnclosingBlock(m_selection.extent()); } TextDirection FrameSelection::directionOfSelection() { // Get bot VisiblePositions first because visibleStart() and visibleEnd() // can cause layout, which has the potential to invalidate lineboxes. auto startPosition = m_selection.visibleStart(); auto endPosition = m_selection.visibleEnd(); auto startRun = startPosition.inlineRunAndOffset().run; auto endRun = endPosition.inlineRunAndOffset().run; if (startRun && endRun && startRun->direction() == endRun->direction()) return startRun->direction(); return directionOfEnclosingBlock(); } void FrameSelection::willBeModified(EAlteration alter, SelectionDirection direction) { if (alter != AlterationExtend) return; Position start = m_selection.start(); Position end = m_selection.end(); bool baseIsStart = true; if (m_selection.isDirectional()) { // 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). if (m_selection.isBaseFirst()) baseIsStart = true; else baseIsStart = false; } else { switch (direction) { case SelectionDirection::Right: if (directionOfSelection() == TextDirection::LTR) baseIsStart = true; else baseIsStart = false; break; case SelectionDirection::Forward: baseIsStart = true; break; case SelectionDirection::Left: if (directionOfSelection() == TextDirection::LTR) baseIsStart = false; else baseIsStart = true; break; case SelectionDirection::Backward: baseIsStart = false; break; } } if (baseIsStart) { m_selection.setBase(start); m_selection.setExtent(end); } else { m_selection.setBase(end); m_selection.setExtent(start); } } VisiblePosition FrameSelection::positionForPlatform(bool isGetStart) const { // 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(). if (m_document && m_document->editor().behavior().shouldAlwaysExtendSelectionFromExtentEndpoint()) return m_selection.isBaseFirst() ? m_selection.visibleEnd() : m_selection.visibleStart(); return isGetStart ? m_selection.visibleStart() : m_selection.visibleEnd(); } VisiblePosition FrameSelection::startForPlatform() const { return positionForPlatform(true); } VisiblePosition FrameSelection::endForPlatform() const { return positionForPlatform(false); } VisiblePosition FrameSelection::nextWordPositionForPlatform(const VisiblePosition& originalPosition) { VisiblePosition positionAfterCurrentWord = nextWordPosition(originalPosition); if (m_document && m_document->editor().behavior().shouldSkipSpaceWhenMovingRight()) { // In order to skip spaces when moving right, we advance one // word further and then move one word back. Given the // semantics of previousWordPosition() this will put us at the // beginning of the word following. VisiblePosition positionAfterSpacingAndFollowingWord = nextWordPosition(positionAfterCurrentWord); if (positionAfterSpacingAndFollowingWord != positionAfterCurrentWord) positionAfterCurrentWord = previousWordPosition(positionAfterSpacingAndFollowingWord); bool movingBackwardsMovedPositionToStartOfCurrentWord = positionAfterCurrentWord == previousWordPosition(nextWordPosition(originalPosition)); if (movingBackwardsMovedPositionToStartOfCurrentWord) positionAfterCurrentWord = positionAfterSpacingAndFollowingWord; } return positionAfterCurrentWord; } static void adjustPositionForUserSelectAll(VisiblePosition& pos, bool isForward) { if (Node* rootUserSelectAll = Position::rootUserSelectAllForNode(pos.deepEquivalent().anchorNode())) pos = isForward ? positionAfterNode(rootUserSelectAll).downstream(CanCrossEditingBoundary) : positionBeforeNode(rootUserSelectAll).upstream(CanCrossEditingBoundary); } VisiblePosition FrameSelection::modifyExtendingRight(TextGranularity granularity) { VisiblePosition pos(m_selection.extent(), m_selection.affinity()); // 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 TextDirection::LTR, // but it extends backward logically if the enclosing block is TextDirection::RTL. switch (granularity) { case TextGranularity::CharacterGranularity: if (directionOfEnclosingBlock() == TextDirection::LTR) pos = pos.next(CannotCrossEditingBoundary); else pos = pos.previous(CannotCrossEditingBoundary); break; case TextGranularity::WordGranularity: if (directionOfEnclosingBlock() == TextDirection::LTR) pos = nextWordPositionForPlatform(pos); else pos = previousWordPosition(pos); break; case TextGranularity::LineBoundary: if (directionOfEnclosingBlock() == TextDirection::LTR) pos = modifyExtendingForward(granularity); else pos = modifyExtendingBackward(granularity); break; case TextGranularity::SentenceGranularity: case TextGranularity::LineGranularity: case TextGranularity::ParagraphGranularity: case TextGranularity::SentenceBoundary: case TextGranularity::ParagraphBoundary: case TextGranularity::DocumentBoundary: // FIXME: implement all of the above? pos = modifyExtendingForward(granularity); break; case TextGranularity::DocumentGranularity: ASSERT_NOT_REACHED(); break; } adjustPositionForUserSelectAll(pos, directionOfEnclosingBlock() == TextDirection::LTR); return pos; } VisiblePosition FrameSelection::modifyExtendingForward(TextGranularity granularity) { VisiblePosition pos(m_selection.extent(), m_selection.affinity()); switch (granularity) { case TextGranularity::CharacterGranularity: pos = pos.next(CannotCrossEditingBoundary); break; case TextGranularity::WordGranularity: pos = nextWordPositionForPlatform(pos); break; case TextGranularity::SentenceGranularity: pos = nextSentencePosition(pos); break; case TextGranularity::LineGranularity: pos = nextLinePosition(pos, lineDirectionPointForBlockDirectionNavigation(Extent)); break; case TextGranularity::ParagraphGranularity: pos = nextParagraphPosition(pos, lineDirectionPointForBlockDirectionNavigation(Extent)); break; case TextGranularity::DocumentGranularity: ASSERT_NOT_REACHED(); break; case TextGranularity::SentenceBoundary: pos = endOfSentence(endForPlatform()); break; case TextGranularity::LineBoundary: pos = logicalEndOfLine(endForPlatform()); break; case TextGranularity::ParagraphBoundary: pos = endOfParagraph(endForPlatform()); break; case TextGranularity::DocumentBoundary: pos = endForPlatform(); if (isEditablePosition(pos.deepEquivalent())) pos = endOfEditableContent(pos); else pos = endOfDocument(pos); break; } adjustPositionForUserSelectAll(pos, directionOfEnclosingBlock() == TextDirection::LTR); return pos; } VisiblePosition FrameSelection::modifyMovingRight(TextGranularity granularity, bool* reachedBoundary) { if (reachedBoundary) *reachedBoundary = false; VisiblePosition pos; switch (granularity) { case TextGranularity::CharacterGranularity: if (isRange()) { if (directionOfSelection() == TextDirection::LTR) pos = VisiblePosition(m_selection.end(), m_selection.affinity()); else pos = VisiblePosition(m_selection.start(), m_selection.affinity()); } else pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).right(true, reachedBoundary); break; case TextGranularity::WordGranularity: { bool skipsSpaceWhenMovingRight = m_document && m_document->editor().behavior().shouldSkipSpaceWhenMovingRight(); VisiblePosition currentPosition(m_selection.extent(), m_selection.affinity()); pos = rightWordPosition(currentPosition, skipsSpaceWhenMovingRight); if (reachedBoundary) *reachedBoundary = pos == currentPosition; break; } case TextGranularity::SentenceGranularity: case TextGranularity::LineGranularity: case TextGranularity::ParagraphGranularity: case TextGranularity::SentenceBoundary: case TextGranularity::ParagraphBoundary: case TextGranularity::DocumentBoundary: // FIXME: Implement all of the above. pos = modifyMovingForward(granularity, reachedBoundary); break; case TextGranularity::LineBoundary: pos = rightBoundaryOfLine(startForPlatform(), directionOfEnclosingBlock(), reachedBoundary); break; case TextGranularity::DocumentGranularity: ASSERT_NOT_REACHED(); break; } return pos; } VisiblePosition FrameSelection::modifyMovingForward(TextGranularity granularity, bool* reachedBoundary) { if (reachedBoundary) *reachedBoundary = false; VisiblePosition currentPosition; switch (granularity) { case TextGranularity::WordGranularity: case TextGranularity::SentenceGranularity: currentPosition = VisiblePosition(m_selection.extent(), m_selection.affinity()); break; case TextGranularity::LineGranularity: case TextGranularity::ParagraphGranularity: case TextGranularity::SentenceBoundary: case TextGranularity::ParagraphBoundary: case TextGranularity::DocumentBoundary: currentPosition = endForPlatform(); break; default: break; } VisiblePosition pos; // FIXME: Stay in editable content for the less common granularities. switch (granularity) { case TextGranularity::CharacterGranularity: if (isRange()) pos = VisiblePosition(m_selection.end(), m_selection.affinity()); else pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).next(CannotCrossEditingBoundary, reachedBoundary); break; case TextGranularity::WordGranularity: pos = nextWordPositionForPlatform(currentPosition); break; case TextGranularity::SentenceGranularity: pos = nextSentencePosition(currentPosition); break; case TextGranularity::LineGranularity: { // 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!) pos = currentPosition; if (!isRange() || !isStartOfLine(pos)) pos = nextLinePosition(pos, lineDirectionPointForBlockDirectionNavigation(Start)); break; } case TextGranularity::ParagraphGranularity: pos = nextParagraphPosition(currentPosition, lineDirectionPointForBlockDirectionNavigation(Start)); break; case TextGranularity::DocumentGranularity: ASSERT_NOT_REACHED(); break; case TextGranularity::SentenceBoundary: pos = endOfSentence(currentPosition); break; case TextGranularity::LineBoundary: pos = logicalEndOfLine(endForPlatform(), reachedBoundary); break; case TextGranularity::ParagraphBoundary: pos = endOfParagraph(currentPosition); break; case TextGranularity::DocumentBoundary: pos = currentPosition; if (isEditablePosition(pos.deepEquivalent())) pos = endOfEditableContent(pos); else pos = endOfDocument(pos); break; } switch (granularity) { case TextGranularity::WordGranularity: case TextGranularity::SentenceGranularity: case TextGranularity::LineGranularity: case TextGranularity::ParagraphGranularity: case TextGranularity::SentenceBoundary: case TextGranularity::ParagraphBoundary: case TextGranularity::DocumentBoundary: if (reachedBoundary) *reachedBoundary = pos == currentPosition; break; default: break; } return pos; } VisiblePosition FrameSelection::modifyExtendingLeft(TextGranularity granularity) { VisiblePosition pos(m_selection.extent(), m_selection.affinity()); // 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 TextDirection::LTR, // but it extends forward logically if the enclosing block is TextDirection::RTL. switch (granularity) { case TextGranularity::CharacterGranularity: if (directionOfEnclosingBlock() == TextDirection::LTR) pos = pos.previous(CannotCrossEditingBoundary); else pos = pos.next(CannotCrossEditingBoundary); break; case TextGranularity::WordGranularity: if (directionOfEnclosingBlock() == TextDirection::LTR) pos = previousWordPosition(pos); else pos = nextWordPositionForPlatform(pos); break; case TextGranularity::LineBoundary: if (directionOfEnclosingBlock() == TextDirection::LTR) pos = modifyExtendingBackward(granularity); else pos = modifyExtendingForward(granularity); break; case TextGranularity::SentenceGranularity: case TextGranularity::LineGranularity: case TextGranularity::ParagraphGranularity: case TextGranularity::SentenceBoundary: case TextGranularity::ParagraphBoundary: case TextGranularity::DocumentBoundary: pos = modifyExtendingBackward(granularity); break; case TextGranularity::DocumentGranularity: ASSERT_NOT_REACHED(); break; } adjustPositionForUserSelectAll(pos, !(directionOfEnclosingBlock() == TextDirection::LTR)); return pos; } VisiblePosition FrameSelection::modifyExtendingBackward(TextGranularity granularity) { VisiblePosition pos(m_selection.extent(), m_selection.affinity()); // 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::CharacterGranularity: pos = pos.previous(CannotCrossEditingBoundary); break; case TextGranularity::WordGranularity: pos = previousWordPosition(pos); break; case TextGranularity::SentenceGranularity: pos = previousSentencePosition(pos); break; case TextGranularity::LineGranularity: pos = previousLinePosition(pos, lineDirectionPointForBlockDirectionNavigation(Extent)); break; case TextGranularity::ParagraphGranularity: pos = previousParagraphPosition(pos, lineDirectionPointForBlockDirectionNavigation(Extent)); break; case TextGranularity::SentenceBoundary: pos = startOfSentence(startForPlatform()); break; case TextGranularity::LineBoundary: pos = logicalStartOfLine(startForPlatform()); break; case TextGranularity::ParagraphBoundary: pos = startOfParagraph(startForPlatform()); break; case TextGranularity::DocumentBoundary: pos = startForPlatform(); if (isEditablePosition(pos.deepEquivalent())) pos = startOfEditableContent(pos); else pos = startOfDocument(pos); break; case TextGranularity::DocumentGranularity: ASSERT_NOT_REACHED(); break; } adjustPositionForUserSelectAll(pos, !(directionOfEnclosingBlock() == TextDirection::LTR)); return pos; } VisiblePosition FrameSelection::modifyMovingLeft(TextGranularity granularity, bool* reachedBoundary) { if (reachedBoundary) *reachedBoundary = false; VisiblePosition pos; switch (granularity) { case TextGranularity::CharacterGranularity: if (isRange()) if (directionOfSelection() == TextDirection::LTR) pos = VisiblePosition(m_selection.start(), m_selection.affinity()); else pos = VisiblePosition(m_selection.end(), m_selection.affinity()); else pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).left(true, reachedBoundary); break; case TextGranularity::WordGranularity: { bool skipsSpaceWhenMovingRight = m_document && m_document->editor().behavior().shouldSkipSpaceWhenMovingRight(); VisiblePosition currentPosition(m_selection.extent(), m_selection.affinity()); pos = leftWordPosition(currentPosition, skipsSpaceWhenMovingRight); if (reachedBoundary) *reachedBoundary = pos == currentPosition; break; } case TextGranularity::SentenceGranularity: case TextGranularity::LineGranularity: case TextGranularity::ParagraphGranularity: case TextGranularity::SentenceBoundary: case TextGranularity::ParagraphBoundary: case TextGranularity::DocumentBoundary: // FIXME: Implement all of the above. pos = modifyMovingBackward(granularity, reachedBoundary); break; case TextGranularity::LineBoundary: pos = leftBoundaryOfLine(startForPlatform(), directionOfEnclosingBlock(), reachedBoundary); break; case TextGranularity::DocumentGranularity: ASSERT_NOT_REACHED(); break; } return pos; } VisiblePosition FrameSelection::modifyMovingBackward(TextGranularity granularity, bool* reachedBoundary) { if (reachedBoundary) *reachedBoundary = false; VisiblePosition currentPosition; switch (granularity) { case TextGranularity::WordGranularity: case TextGranularity::SentenceGranularity: currentPosition = VisiblePosition(m_selection.extent(), m_selection.affinity()); break; case TextGranularity::LineGranularity: case TextGranularity::ParagraphGranularity: case TextGranularity::SentenceBoundary: case TextGranularity::ParagraphBoundary: case TextGranularity::DocumentBoundary: currentPosition = startForPlatform(); break; default: break; } VisiblePosition pos; switch (granularity) { case TextGranularity::CharacterGranularity: if (isRange()) pos = VisiblePosition(m_selection.start(), m_selection.affinity()); else pos = VisiblePosition(m_selection.extent(), m_selection.affinity()).previous(CannotCrossEditingBoundary, reachedBoundary); break; case TextGranularity::WordGranularity: pos = previousWordPosition(currentPosition); break; case TextGranularity::SentenceGranularity: pos = previousSentencePosition(currentPosition); break; case TextGranularity::LineGranularity: pos = previousLinePosition(currentPosition, lineDirectionPointForBlockDirectionNavigation(Start)); break; case TextGranularity::ParagraphGranularity: pos = previousParagraphPosition(currentPosition, lineDirectionPointForBlockDirectionNavigation(Start)); break; case TextGranularity::SentenceBoundary: pos = startOfSentence(currentPosition); break; case TextGranularity::LineBoundary: pos = logicalStartOfLine(startForPlatform(), reachedBoundary); break; case TextGranularity::ParagraphBoundary: pos = startOfParagraph(currentPosition); break; case TextGranularity::DocumentBoundary: pos = currentPosition; if (isEditablePosition(pos.deepEquivalent())) pos = startOfEditableContent(pos); else pos = startOfDocument(pos); break; case TextGranularity::DocumentGranularity: ASSERT_NOT_REACHED(); break; } switch (granularity) { case TextGranularity::WordGranularity: case TextGranularity::SentenceGranularity: case TextGranularity::LineGranularity: case TextGranularity::ParagraphGranularity: case TextGranularity::SentenceBoundary: case TextGranularity::ParagraphBoundary: case TextGranularity::DocumentBoundary: if (reachedBoundary) *reachedBoundary = pos == currentPosition; break; default: break; } return pos; } static bool isBoundary(TextGranularity granularity) { return granularity == TextGranularity::LineBoundary || granularity == TextGranularity::ParagraphBoundary || granularity == TextGranularity::DocumentBoundary; } AXTextStateChangeIntent FrameSelection::textSelectionIntent(EAlteration alter, SelectionDirection direction, TextGranularity granularity) { AXTextStateChangeIntent intent = AXTextStateChangeIntent(); bool flip = false; if (alter == FrameSelection::AlterationMove) { intent.type = AXTextStateChangeTypeSelectionMove; flip = isRange() && directionOfSelection() == TextDirection::RTL; } else intent.type = AXTextStateChangeTypeSelectionExtend; switch (granularity) { case TextGranularity::CharacterGranularity: intent.selection.granularity = AXTextSelectionGranularityCharacter; break; case TextGranularity::WordGranularity: intent.selection.granularity = AXTextSelectionGranularityWord; break; case TextGranularity::SentenceGranularity: case TextGranularity::SentenceBoundary: intent.selection.granularity = AXTextSelectionGranularitySentence; break; case TextGranularity::LineGranularity: case TextGranularity::LineBoundary: intent.selection.granularity = AXTextSelectionGranularityLine; break; case TextGranularity::ParagraphGranularity: case TextGranularity::ParagraphBoundary: intent.selection.granularity = AXTextSelectionGranularityParagraph; break; case TextGranularity::DocumentGranularity: case TextGranularity::DocumentBoundary: intent.selection.granularity = AXTextSelectionGranularityDocument; break; } bool boundary = false; switch (granularity) { case TextGranularity::CharacterGranularity: case TextGranularity::WordGranularity: case TextGranularity::SentenceGranularity: case TextGranularity::LineGranularity: case TextGranularity::ParagraphGranularity: case TextGranularity::DocumentGranularity: break; case TextGranularity::SentenceBoundary: case TextGranularity::LineBoundary: case TextGranularity::ParagraphBoundary: case TextGranularity::DocumentBoundary: boundary = true; break; } switch (direction) { case SelectionDirection::Right: case SelectionDirection::Forward: if (boundary) intent.selection.direction = flip ? AXTextSelectionDirectionBeginning : AXTextSelectionDirectionEnd; else intent.selection.direction = flip ? AXTextSelectionDirectionPrevious : AXTextSelectionDirectionNext; break; case SelectionDirection::Left: case SelectionDirection::Backward: if (boundary) intent.selection.direction = flip ? AXTextSelectionDirectionEnd : AXTextSelectionDirectionBeginning; else intent.selection.direction = flip ? AXTextSelectionDirectionNext : AXTextSelectionDirectionPrevious; break; } return intent; } static AXTextSelection textSelectionWithDirectionAndGranularity(SelectionDirection direction, TextGranularity granularity) { // FIXME: Account for BIDI in SelectionDirection::Right & SelectionDirection::Left. (In a RTL block, Right would map to Previous/Beginning and Left to Next/End.) AXTextSelectionDirection intentDirection = AXTextSelectionDirectionUnknown; switch (direction) { case SelectionDirection::Forward: intentDirection = AXTextSelectionDirectionNext; break; case SelectionDirection::Right: intentDirection = AXTextSelectionDirectionNext; break; case SelectionDirection::Backward: intentDirection = AXTextSelectionDirectionPrevious; break; case SelectionDirection::Left: intentDirection = AXTextSelectionDirectionPrevious; break; } AXTextSelectionGranularity intentGranularity = AXTextSelectionGranularityUnknown; switch (granularity) { case TextGranularity::CharacterGranularity: intentGranularity = AXTextSelectionGranularityCharacter; break; case TextGranularity::WordGranularity: intentGranularity = AXTextSelectionGranularityWord; break; case TextGranularity::SentenceGranularity: case TextGranularity::SentenceBoundary: // FIXME: Boundary should affect direction. intentGranularity = AXTextSelectionGranularitySentence; break; case TextGranularity::LineGranularity: intentGranularity = AXTextSelectionGranularityLine; break; case TextGranularity::ParagraphGranularity: case TextGranularity::ParagraphBoundary: // FIXME: Boundary should affect direction. intentGranularity = AXTextSelectionGranularityParagraph; break; case TextGranularity::DocumentGranularity: case TextGranularity::DocumentBoundary: // FIXME: Boundary should affect direction. intentGranularity = AXTextSelectionGranularityDocument; break; case TextGranularity::LineBoundary: intentGranularity = AXTextSelectionGranularityLine; switch (direction) { case SelectionDirection::Forward: intentDirection = AXTextSelectionDirectionEnd; break; case SelectionDirection::Right: intentDirection = AXTextSelectionDirectionEnd; break; case SelectionDirection::Backward: intentDirection = AXTextSelectionDirectionBeginning; break; case SelectionDirection::Left: intentDirection = AXTextSelectionDirectionBeginning; break; } break; } return { intentDirection, intentGranularity, false }; } bool FrameSelection::modify(EAlteration alter, SelectionDirection direction, TextGranularity granularity, EUserTriggered userTriggered) { if (userTriggered == UserTriggered) { FrameSelection trialFrameSelection; trialFrameSelection.setSelection(m_selection); trialFrameSelection.modify(alter, direction, granularity, NotUserTriggered); bool change = shouldChangeSelection(trialFrameSelection.selection()); if (!change) return false; if (trialFrameSelection.selection().isRange() && m_selection.isCaret() && !dispatchSelectStart()) return false; } willBeModified(alter, direction); bool reachedBoundary = false; bool wasRange = m_selection.isRange(); Position originalStartPosition = m_selection.start(); VisiblePosition position; switch (direction) { case SelectionDirection::Right: if (alter == AlterationMove) position = modifyMovingRight(granularity, &reachedBoundary); else position = modifyExtendingRight(granularity); break; case SelectionDirection::Forward: if (alter == AlterationExtend) position = modifyExtendingForward(granularity); else position = modifyMovingForward(granularity, &reachedBoundary); break; case SelectionDirection::Left: if (alter == AlterationMove) position = modifyMovingLeft(granularity, &reachedBoundary); else position = modifyExtendingLeft(granularity); break; case SelectionDirection::Backward: if (alter == AlterationExtend) position = modifyExtendingBackward(granularity); else position = modifyMovingBackward(granularity, &reachedBoundary); break; } if (reachedBoundary && !isRange() && userTriggered == UserTriggered && m_document && AXObjectCache::accessibilityEnabled()) { notifyAccessibilityForSelectionChange({ AXTextStateChangeTypeSelectionBoundary, textSelectionWithDirectionAndGranularity(direction, granularity) }); return true; } if (position.isNull()) return false; if (m_document && isSpatialNavigationEnabled(m_document->frame())) { if (!wasRange && alter == AlterationMove && position == originalStartPosition) return false; } if (m_document && AXObjectCache::accessibilityEnabled()) { if (AXObjectCache* cache = m_document->existingAXObjectCache()) cache->setTextSelectionIntent(textSelectionIntent(alter, direction, granularity)); } // 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(Start); m_selection.setIsDirectional(shouldAlwaysUseDirectionalSelection(m_document.get()) || alter == AlterationExtend); switch (alter) { case AlterationMove: moveTo(position, userTriggered); break; case AlterationExtend: if (!m_selection.isCaret() && (granularity == TextGranularity::WordGranularity || granularity == TextGranularity::ParagraphGranularity || granularity == TextGranularity::LineGranularity) && m_document && !m_document->editor().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. VisibleSelection newSelection = m_selection; newSelection.setExtent(position); if (m_selection.isBaseFirst() != newSelection.isBaseFirst()) position = m_selection.base(); } // 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 (!m_document || !m_document->editor().behavior().shouldAlwaysGrowSelectionWhenExtendingToBoundary() || m_selection.isCaret() || !isBoundary(granularity)) setExtent(position, userTriggered); else { TextDirection textDirection = directionOfEnclosingBlock(); if (direction == SelectionDirection::Forward || (textDirection == TextDirection::LTR && direction == SelectionDirection::Right) || (textDirection == TextDirection::RTL && direction == SelectionDirection::Left)) setEnd(position, userTriggered); else setStart(position, userTriggered); } break; } if (granularity == TextGranularity::LineGranularity || granularity == TextGranularity::ParagraphGranularity) m_xPosForVerticalArrowNavigation = x; if (userTriggered == UserTriggered) m_granularity = TextGranularity::CharacterGranularity; setCaretRectNeedsUpdate(); return true; } // FIXME: Maybe baseline would be better? static bool absoluteCaretY(const VisiblePosition& c, int& y) { IntRect rect = c.absoluteCaretBounds(); if (rect.isEmpty()) return false; y = rect.y() + rect.height() / 2; return true; } bool FrameSelection::modify(EAlteration alter, unsigned verticalDistance, VerticalDirection direction, EUserTriggered userTriggered, CursorAlignOnScroll align) { if (!verticalDistance) return false; if (userTriggered == UserTriggered) { FrameSelection trialFrameSelection; trialFrameSelection.setSelection(m_selection); trialFrameSelection.modify(alter, verticalDistance, direction, NotUserTriggered); bool change = shouldChangeSelection(trialFrameSelection.selection()); if (!change) return false; } willBeModified(alter, direction == DirectionUp ? SelectionDirection::Backward : SelectionDirection::Forward); VisiblePosition pos; LayoutUnit xPos; switch (alter) { case AlterationMove: pos = VisiblePosition(direction == DirectionUp ? m_selection.start() : m_selection.end(), m_selection.affinity()); xPos = lineDirectionPointForBlockDirectionNavigation(direction == DirectionUp ? Start : End); m_selection.setAffinity(direction == DirectionUp ? Affinity::Upstream : Affinity::Downstream); break; case AlterationExtend: pos = VisiblePosition(m_selection.extent(), m_selection.affinity()); xPos = lineDirectionPointForBlockDirectionNavigation(Extent); m_selection.setAffinity(Affinity::Downstream); break; } int startY; if (!absoluteCaretY(pos, startY)) return false; if (direction == DirectionUp) startY = -startY; int lastY = startY; VisiblePosition result; VisiblePosition next; for (VisiblePosition p = pos; ; p = next) { if (direction == DirectionUp) next = previousLinePosition(p, xPos); else next = nextLinePosition(p, xPos); if (next.isNull() || next == p) break; int nextY; if (!absoluteCaretY(next, nextY)) break; if (direction == DirectionUp) nextY = -nextY; if (nextY - startY > static_cast(verticalDistance)) break; if (nextY >= lastY) { lastY = nextY; result = next; } } if (result.isNull()) return false; switch (alter) { case AlterationMove: moveTo(result, userTriggered, align); break; case AlterationExtend: setExtent(result, userTriggered); break; } if (userTriggered == UserTriggered) m_granularity = TextGranularity::CharacterGranularity; m_selection.setIsDirectional(shouldAlwaysUseDirectionalSelection(m_document.get()) || alter == AlterationExtend); return true; } LayoutUnit FrameSelection::lineDirectionPointForBlockDirectionNavigation(PositionType type) { if (isNone()) return 0; // FIXME: Can we use visibleStart/End/Extent? Position position; switch (type) { case Start: position = m_selection.start(); break; case End: position = m_selection.end(); break; case Extent: position = m_selection.extent(); break; } // FIXME: Why is this check needed? What's the harm in doing a little more work without a frame? if (!position.anchorNode()->document().frame()) return 0; // FIXME: Can we do this before getting the position from the selection? if (m_xPosForVerticalArrowNavigation) return *m_xPosForVerticalArrowNavigation; // 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. VisiblePosition visiblePosition(position, m_selection.affinity()); auto x = visiblePosition.isNotNull() ? visiblePosition.lineDirectionPointForBlockDirectionNavigation() : 0; m_xPosForVerticalArrowNavigation = { x }; return x; } void FrameSelection::clear() { m_granularity = TextGranularity::CharacterGranularity; setSelection(VisibleSelection()); } void FrameSelection::willBeRemovedFromFrame() { m_granularity = TextGranularity::CharacterGranularity; #if ENABLE(TEXT_CARET) m_caretBlinkTimer.stop(); #endif if (auto* view = m_document->renderView()) view->selection().clear(); setSelectionWithoutUpdatingAppearance(VisibleSelection(), defaultSetSelectionOptions(), AlignCursorOnScrollIfNeeded, TextGranularity::CharacterGranularity); m_previousCaretNode = nullptr; m_typingStyle = nullptr; m_appearanceUpdateTimer.stop(); } void FrameSelection::setStart(const VisiblePosition& position, EUserTriggered trigger) { if (m_selection.isBaseFirst()) setBase(position, trigger); else setExtent(position, trigger); } void FrameSelection::setEnd(const VisiblePosition& position, EUserTriggered trigger) { if (m_selection.isBaseFirst()) setExtent(position, trigger); else setBase(position, trigger); } void FrameSelection::setBase(const VisiblePosition& position, EUserTriggered userTriggered) { const bool selectionHasDirection = true; setSelection(VisibleSelection(position.deepEquivalent(), m_selection.extent(), position.affinity(), selectionHasDirection), defaultSetSelectionOptions(userTriggered)); } void FrameSelection::setExtent(const VisiblePosition& position, EUserTriggered userTriggered) { const bool selectionHasDirection = true; setSelection(VisibleSelection(m_selection.base(), position.deepEquivalent(), position.affinity(), selectionHasDirection), defaultSetSelectionOptions(userTriggered)); } void FrameSelection::setBase(const Position& position, Affinity affinity, EUserTriggered userTriggered) { const bool selectionHasDirection = true; setSelection(VisibleSelection(position, m_selection.extent(), affinity, selectionHasDirection), defaultSetSelectionOptions(userTriggered)); } void FrameSelection::setExtent(const Position& position, Affinity affinity, EUserTriggered userTriggered) { const bool selectionHasDirection = true; setSelection(VisibleSelection(m_selection.base(), position, affinity, selectionHasDirection), defaultSetSelectionOptions(userTriggered)); } void CaretBase::clearCaretRect() { m_caretLocalRect = LayoutRect(); } bool CaretBase::updateCaretRect(Document& document, const VisiblePosition& caretPosition) { document.updateLayoutIgnorePendingStylesheets(); m_caretRectNeedsUpdate = false; RenderBlock* renderer; m_caretLocalRect = localCaretRectInRendererForCaretPainting(caretPosition, renderer); return !m_caretLocalRect.isEmpty(); } RenderBlock* FrameSelection::caretRendererWithoutUpdatingLayout() const { return rendererForCaretPainting(m_selection.start().deprecatedNode()); } RenderBlock* DragCaretController::caretRenderer() const { return rendererForCaretPainting(m_position.deepEquivalent().deprecatedNode()); } static bool isNonOrphanedCaret(const VisibleSelection& selection) { return selection.isCaret() && !selection.start().isOrphan() && !selection.end().isOrphan(); } IntRect FrameSelection::absoluteCaretBounds(bool* insideFixed) { if (!m_document) return IntRect(); updateSelectionByUpdatingLayoutOrStyle(*m_document); recomputeCaretRect(); if (insideFixed) *insideFixed = m_caretInsidePositionFixed; return m_absCaretBounds; } static void repaintCaretForLocalRect(Node* node, const LayoutRect& rect) { if (auto* caretPainter = rendererForCaretPainting(node)) caretPainter->repaintRectangle(rect); } bool FrameSelection::recomputeCaretRect() { if (!shouldUpdateCaretRect()) return false; if (!m_document) return false; FrameView* v = m_document->view(); if (!v) return false; LayoutRect oldRect = localCaretRectWithoutUpdate(); RefPtr caretNode = m_previousCaretNode; if (shouldUpdateCaretRect()) { if (!isNonOrphanedCaret(m_selection)) clearCaretRect(); else { VisiblePosition visibleStart = m_selection.visibleStart(); if (updateCaretRect(*m_document, visibleStart)) { caretNode = visibleStart.deepEquivalent().deprecatedNode(); m_absCaretBoundsDirty = true; } } } LayoutRect newRect = localCaretRectWithoutUpdate(); if (caretNode == m_previousCaretNode && oldRect == newRect && !m_absCaretBoundsDirty) return false; IntRect oldAbsCaretBounds = m_absCaretBounds; bool isInsideFixed; m_absCaretBounds = absoluteBoundsForLocalCaretRect(rendererForCaretPainting(caretNode.get()), newRect, &isInsideFixed); m_caretInsidePositionFixed = isInsideFixed; if (m_absCaretBoundsDirty && m_selection.isCaret()) // We should be able to always assert this condition. ASSERT(m_absCaretBounds == m_selection.visibleStart().absoluteCaretBounds()); m_absCaretBoundsDirty = false; if (caretNode == m_previousCaretNode && oldAbsCaretBounds == m_absCaretBounds) return false; #if ENABLE(TEXT_CARET) if (RenderView* view = m_document->renderView()) { bool previousOrNewCaretNodeIsContentEditable = m_selection.isContentEditable() || (m_previousCaretNode && m_previousCaretNode->isContentEditable()); if (shouldRepaintCaret(view, previousOrNewCaretNodeIsContentEditable)) { if (m_previousCaretNode) repaintCaretForLocalRect(m_previousCaretNode.get(), oldRect); m_previousCaretNode = caretNode; repaintCaretForLocalRect(caretNode.get(), newRect); } } #endif return true; } bool CaretBase::shouldRepaintCaret(const RenderView* view, bool isContentEditable) const { ASSERT(view); Frame* frame = &view->frameView().frame(); // The frame where the selection started. bool caretBrowsing = frame && frame->settings().caretBrowsingEnabled(); return (caretBrowsing || isContentEditable); } void FrameSelection::invalidateCaretRect() { if (!isCaret()) return; CaretBase::invalidateCaretRect(m_selection.start().deprecatedNode(), recomputeCaretRect()); } void CaretBase::invalidateCaretRect(Node* node, bool caretRectChanged) { // EDIT FIXME: This is an unfortunate hack. // Basically, we can't trust this layout position since we // can't guarantee that the check to see if we are in unrendered // content will work at this point. We may have to wait for // a layout and re-render of the document to happen. So, resetting this // flag will cause another caret layout to happen the first time // that we try to paint the caret after this call. That one will work since // it happens after the document has accounted for any editing // changes which may have been done. // And, we need to leave this layout here so the caret moves right // away after clicking. m_caretRectNeedsUpdate = true; if (caretRectChanged) return; if (RenderView* view = node->document().renderView()) { if (shouldRepaintCaret(view, isEditableNode(*node))) repaintCaretForLocalRect(node, localCaretRectWithoutUpdate()); } } void FrameSelection::paintCaret(GraphicsContext& context, const LayoutPoint& paintOffset, const LayoutRect& clipRect) { if (m_selection.isCaret() && m_caretPaint && m_selection.start().deprecatedNode()) CaretBase::paintCaret(*m_selection.start().deprecatedNode(), context, paintOffset, clipRect); } Color CaretBase::computeCaretColor(const RenderStyle& elementStyle, const Node* node) { // On iOS, we want to fall back to the tintColor, and only override if CSS has explicitly specified a custom color. #if PLATFORM(IOS_FAMILY) && !PLATFORM(MACCATALYST) UNUSED_PARAM(node); return elementStyle.caretColor(); #else auto* rootEditableElement = node ? node->rootEditableElement() : nullptr; auto* rootEditableStyle = rootEditableElement && rootEditableElement->renderer() ? &rootEditableElement->renderer()->style() : nullptr; // CSS value "auto" is treated as an invalid color. if (!elementStyle.caretColor().isValid() && rootEditableStyle) { auto rootEditableBackgroundColor = rootEditableStyle->visitedDependentColorWithColorFilter(CSSPropertyBackgroundColor); auto elementBackgroundColor = elementStyle.visitedDependentColorWithColorFilter(CSSPropertyBackgroundColor); auto disappearsIntoBackground = blendSourceOver(rootEditableBackgroundColor, elementBackgroundColor) == rootEditableBackgroundColor; if (disappearsIntoBackground) return rootEditableStyle->visitedDependentColorWithColorFilter(CSSPropertyCaretColor); } return elementStyle.visitedDependentColorWithColorFilter(CSSPropertyCaretColor); #endif } void CaretBase::paintCaret(const Node& node, GraphicsContext& context, const LayoutPoint& paintOffset, const LayoutRect& clipRect) const { #if ENABLE(TEXT_CARET) if (m_caretVisibility == Hidden) return; auto drawingRect = localCaretRectWithoutUpdate(); if (auto* renderer = rendererForCaretPainting(&node)) renderer->flipForWritingMode(drawingRect); drawingRect.moveBy(paintOffset); auto caret = intersection(drawingRect, clipRect); if (caret.isEmpty()) return; Color caretColor = Color::black; auto* element = is(node) ? downcast(&node) : node.parentElement(); if (element && element->renderer()) caretColor = CaretBase::computeCaretColor(element->renderer()->style(), &node); auto pixelSnappedCaretRect = snapRectToDevicePixels(caret, node.document().deviceScaleFactor()); context.fillRect(pixelSnappedCaretRect, caretColor); #else UNUSED_PARAM(node); UNUSED_PARAM(context); UNUSED_PARAM(paintOffset); UNUSED_PARAM(clipRect); #endif } void FrameSelection::debugRenderer(RenderObject* renderer, bool selected) const { if (is(*renderer->node())) { Element& element = downcast(*renderer->node()); fprintf(stderr, "%s%s\n", selected ? "==> " : " ", element.localName().string().utf8().data()); } else if (is(*renderer)) { RenderText& textRenderer = downcast(*renderer); if (textRenderer.text().isEmpty() || !textRenderer.firstTextBox()) { fprintf(stderr, "%s#text (empty)\n", selected ? "==> " : " "); return; } static const int max = 36; String text = textRenderer.text(); int textLength = text.length(); if (selected) { int offset = 0; if (renderer->node() == m_selection.start().containerNode()) offset = m_selection.start().computeOffsetInContainerNode(); else if (renderer->node() == m_selection.end().containerNode()) offset = m_selection.end().computeOffsetInContainerNode(); int pos; LegacyInlineTextBox* box = textRenderer.findNextInlineTextBox(offset, pos); text = text.substring(box->start(), box->len()); String show; int mid = max / 2; int caret = 0; // text is shorter than max if (textLength < max) { show = text; caret = pos; } else if (pos - mid < 0) { // too few characters to left show = text.left(max - 3) + "..."; caret = pos; } else if (pos - mid >= 0 && pos + mid <= textLength) { // enough characters on each side show = "..." + text.substring(pos - mid + 3, max - 6) + "..."; caret = mid; } else { // too few characters on right show = "..." + text.right(max - 3); caret = pos - (textLength - show.length()); } show.replace('\n', ' '); show.replace('\r', ' '); fprintf(stderr, "==> #text : \"%s\" at offset %d\n", show.utf8().data(), pos); fprintf(stderr, " "); for (int i = 0; i < caret; i++) fprintf(stderr, " "); fprintf(stderr, "^\n"); } else { if ((int)text.length() > max) text = text.left(max - 3) + "..."; else text = text.left(max); fprintf(stderr, " #text : \"%s\"\n", text.utf8().data()); } } } bool FrameSelection::contains(const LayoutPoint& point) const { // Treat a collapsed selection like no selection. if (!isRange()) return false; auto range = m_selection.firstRange(); if (!range) return false; if (!m_document) return false; HitTestResult result(point); m_document->hitTest(HitTestRequest(), result); RefPtr innerNode = result.innerNode(); if (!innerNode || !innerNode->renderer()) return false; if (HTMLElement::isInsideImageOverlay(*range) && HTMLElement::isInsideImageOverlay(*innerNode)) { for (auto quad : RenderObject::absoluteTextQuads(*range, { RenderObject::BoundingRectBehavior::UseSelectionHeight })) { if (!quad.isEmpty() && quad.containsPoint(point)) return true; } return false; } return WebCore::contains(*range, makeBoundaryPoint(innerNode->renderer()->positionForPoint(result.localPoint(), nullptr))); } // Workaround for the fact that it's hard to delete a frame. // Call this after doing user-triggered selections to make it easy to delete the frame you entirely selected. // Can't do this implicitly as part of every setSelection call because in some contexts it might not be good // for the focus to move to another frame. So instead we call it from places where we are selecting with the // mouse or the keyboard after setting the selection. void FrameSelection::selectFrameElementInParentIfFullySelected() { // Find the parent frame; if there is none, then we have nothing to do. auto document = makeRefPtr(m_document.get()); if (!document) return; auto frame = makeRefPtr(document->frame()); if (!frame) return; auto parent = makeRefPtr(frame->tree().parent()); if (!parent) return; Page* page = m_document->page(); if (!page) return; // Check if the selection contains the entire frame contents; if not, then there is nothing to do. if (!isRange()) return; if (!isStartOfDocument(selection().visibleStart())) return; if (!isEndOfDocument(selection().visibleEnd())) return; // Get to the