blob: 74d616fcd7daf7fe4bd8f6e3aac5f0a8dbdc2c03 [file] [log] [blame]
/*
* Copyright (C) 2004-2025 Apple Inc. All rights reserved.
* Copyright (C) 2015 Google 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 "BoundaryPointInlines.h"
#include "CaretAnimator.h"
#include "CharacterData.h"
#include "ColorBlending.h"
#include "ContainerNodeInlines.h"
#include "DeleteSelectionCommand.h"
#include "DictationCaretAnimator.h"
#include "DocumentInlines.h"
#include "DocumentPage.h"
#include "DocumentQuirks.h"
#include "DocumentView.h"
#include "Editing.h"
#include "Editor.h"
#include "EditorClient.h"
#include "Element.h"
#include "ElementAncestorIteratorInlines.h"
#include "Event.h"
#include "EventNames.h"
#include "FloatQuad.h"
#include "FocusController.h"
#include "FrameDestructionObserverInlines.h"
#include "FrameTree.h"
#include "GCReachableRef.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 "ImageOverlay.h"
#include "InlineRunAndOffset.h"
#include "LegacyInlineTextBox.h"
#include "LocalDOMWindow.h"
#include "LocalFrame.h"
#include "LocalFrameView.h"
#include "Logging.h"
#include "MutableStyleProperties.h"
#include "NodeInlines.h"
#include "OpacityCaretAnimator.h"
#include "PositionInlines.h"
#include "PseudoClassChangeInvalidation.h"
#include "Range.h"
#include "RenderLayer.h"
#include "RenderLayerScrollableArea.h"
#include "RenderStyle+GettersInlines.h"
#include "RenderText.h"
#include "RenderTextControl.h"
#include "RenderTheme.h"
#include "RenderView.h"
#include "RenderWidget.h"
#include "RenderedPosition.h"
#include "ScriptDisallowedScope.h"
#include "SelectionGeometry.h"
#include "Settings.h"
#include "ShadowRoot.h"
#include "SimpleCaretAnimator.h"
#include "SimpleRange.h"
#include "StyleProperties.h"
#include "StyleTreeResolver.h"
#include "TypedElementDescendantIteratorInlines.h"
#include "TypingCommand.h"
#include "VisibleUnits.h"
#include <stdio.h>
#include <wtf/TZoneMallocInlines.h>
#include <wtf/text/CString.h>
#include <wtf/text/TextStream.h>
#if PLATFORM(IOS_FAMILY)
#include "Chrome.h"
#include "ChromeClient.h"
#include "Color.h"
#include "RenderObject.h"
#include "RenderStyle.h"
#endif
namespace WebCore {
WTF_MAKE_TZONE_ALLOCATED_IMPL(CaretBase);
WTF_MAKE_TZONE_ALLOCATED_IMPL(DragCaretController);
WTF_MAKE_TZONE_ALLOCATED_IMPL(FrameSelection);
using namespace HTMLNames;
CaretBase::CaretBase(CaretVisibility visibility)
: m_caretRectNeedsUpdate(true)
, m_caretVisibility(visibility)
{
}
DragCaretController::DragCaretController()
: CaretBase(CaretVisibility::Visible)
{
}
bool DragCaretController::isContentRichlyEditable() const
{
return isRichlyEditablePosition(m_position.deepEquivalent());
}
IntRect DragCaretController::caretRectInRootViewCoordinates() const
{
if (!hasCaret())
return { };
if (RefPtr document = m_position.deepEquivalent().document()) {
if (RefPtr documentView = document->view())
return documentView->contentsToRootView(m_position.absoluteCaretBounds());
}
return { };
}
IntRect DragCaretController::editableElementRectInRootViewCoordinates() const
{
if (!hasCaret())
return { };
RefPtr<ContainerNode> editableContainer;
if (RefPtr formControl = enclosingTextFormControl(m_position.deepEquivalent()))
editableContainer = WTF::move(formControl);
else
editableContainer = highestEditableRoot(m_position.deepEquivalent());
if (!editableContainer)
return { };
CheckedPtr renderer = editableContainer->renderer();
if (!renderer)
return { };
if (RefPtr 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->editingBehavior().shouldConsiderSelectionAsDirectional();
}
static inline bool isPageActive(Document* document)
{
return document && document->page() && document->page()->focusController().isActive();
}
static UniqueRef<CaretAnimator> createCaretAnimator(FrameSelection* frameSelection, std::optional<CaretAnimatorType> optionalCaretType = std::nullopt)
{
#if PLATFORM(MAC) && HAVE(REDESIGNED_TEXT_CURSOR)
if (redesignedTextCursorEnabled()) {
std::optional<LayoutRect> existingExpansionRect = std::nullopt;
if (optionalCaretType)
existingExpansionRect = frameSelection->caretAnimator().caretRepaintRectForLocalRect(LayoutRect());
switch (optionalCaretType.value_or(CaretAnimatorType::Default)) {
case CaretAnimatorType::Default:
return makeUniqueRef<OpacityCaretAnimator>(*frameSelection, existingExpansionRect);
case CaretAnimatorType::Dictation:
return makeUniqueRef<DictationCaretAnimator>(*frameSelection);
}
}
#else
UNUSED_PARAM(optionalCaretType);
#endif
return makeUniqueRef<SimpleCaretAnimator>(*frameSelection);
}
FrameSelection::FrameSelection(Document* document)
: m_document(document)
, m_granularity(TextGranularity::CharacterGranularity)
, m_caretAnimator(createCaretAnimator(this))
, m_caretInsidePositionFixed(false)
, m_absCaretBoundsDirty(true)
, m_focused(document && document->frame() && document->page() && document->page()->focusController().focusedLocalFrame() == document->frame())
, m_isActive(isPageActive(document))
, m_shouldShowBlockCursor(false)
, m_pendingSelectionUpdate(false)
, m_alwaysAlignCursorOnScrollWhenRevealingSelection(false)
#if PLATFORM(IOS_FAMILY)
, m_updateAppearanceEnabled(false)
#endif
{
if (shouldAlwaysUseDirectionalSelection(m_document.get()))
m_selection.setDirectionality(Directionality::Strong);
bool activeAndFocused = isFocusedAndActive();
#if USE(UIKIT_EDITING)
constexpr auto shouldUpdateAppearance = ShouldUpdateAppearance::Yes;
#else
constexpr auto shouldUpdateAppearance = ShouldUpdateAppearance::No;
#endif
// Caret blinking (blinks | does not blink)
if (activeAndFocused) {
setSelectionFromNone();
removeCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::IsNotFocusedOrActive, shouldUpdateAppearance);
} else
addCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::IsNotFocusedOrActive, shouldUpdateAppearance);
}
FrameSelection::~FrameSelection() = default;
Element* FrameSelection::rootEditableElementOrDocumentElement() const
{
SUPPRESS_UNCOUNTED_LOCAL auto* selectionRoot = m_selection.rootEditableElement();
return selectionRoot ? selectionRoot : m_document->documentElement();
}
void FrameSelection::moveTo(const VisiblePosition& position, UserTriggered userTriggered, CursorAlignOnScroll align)
{
setSelection(VisibleSelection(position.deepEquivalent(), position.deepEquivalent(), position.affinity(), m_selection.directionality()),
defaultSetSelectionOptions(userTriggered), AXTextStateChangeIntent(), align);
}
void FrameSelection::moveTo(const VisiblePosition& base, const VisiblePosition& extent, UserTriggered userTriggered)
{
setSelection(VisibleSelection(base.deepEquivalent(), extent.deepEquivalent(), base.affinity(), Directionality::Strong), defaultSetSelectionOptions(userTriggered));
}
void FrameSelection::moveTo(const Position& position, Affinity affinity, UserTriggered userTriggered)
{
setSelection(VisibleSelection(position, affinity, m_selection.directionality()), defaultSetSelectionOptions(userTriggered));
}
void FrameSelection::moveTo(const Position& base, const Position& extent, Affinity affinity, UserTriggered userTriggered)
{
setSelection(VisibleSelection(base, extent, affinity, Directionality::Strong), defaultSetSelectionOptions(userTriggered));
}
void FrameSelection::moveWithoutValidationTo(const Position& base, const Position& extent, bool selectionHasDirection, OptionSet<SetSelectionOption> options, const AXTextStateChangeIntent& intent)
{
VisibleSelection newSelection;
newSelection.setWithoutValidation(base, extent);
newSelection.setDirectionality(selectionHasDirection ? Directionality::Strong : Directionality::None);
AXTextStateChangeIntent newIntent = intent.type == AXTextStateChangeType::Unknown ? AXTextStateChangeIntent(AXTextStateChangeType::SelectionMove, AXTextSelection { AXTextSelectionDirection::Discontiguous, AXTextSelectionGranularity::Unknown, false }) : intent;
setSelection(newSelection, options, newIntent, CursorAlignOnScroll::IfNeeded, TextGranularity::CharacterGranularity);
}
void DragCaretController::setCaretPosition(const VisiblePosition& position)
{
if (RefPtr node = m_position.deepEquivalent().deprecatedNode())
invalidateCaretRect(node.get());
m_position = position;
setCaretRectNeedsUpdate();
RefPtr<Document> document;
if (RefPtr node = m_position.deepEquivalent().deprecatedNode()) {
invalidateCaretRect(node.get());
document = node->document();
}
if (m_position.isNull() || m_position.isOrphan())
clearCaretRect();
else
updateCaretRect(*document, m_position);
}
static void adjustEndpointsAtBidiBoundary(VisiblePosition& visibleBase, VisiblePosition& visibleExtent)
{
// FIXME: Consider unifying with the logic in `adjustVisibleExtentPreservingVisualContiguity`, so that we
// expand the selection to the nearest range that maintains logical and visual contiguity.
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;
auto directionality = shouldAlwaysUseDirectionalSelection(m_document.get()) ? Directionality::Strong : newSelection.directionality();
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.setDirectionality(directionality);
if (m_selection == newSelection || !shouldChangeSelection(newSelection))
return;
AXTextStateChangeIntent intent;
if (AXObjectCache::accessibilityEnabled() && newSelection.isCaret())
intent = AXTextStateChangeIntent(AXTextStateChangeType::SelectionMove, AXTextSelection { AXTextSelectionDirection::Discontiguous, AXTextSelectionGranularity::Unknown, false });
else
intent = AXTextStateChangeIntent();
setSelection(newSelection, defaultSetSelectionOptions() | SetSelectionOption::FireSelectEvent, intent, CursorAlignOnScroll::IfNeeded, granularity);
}
bool FrameSelection::setSelectionWithoutUpdatingAppearance(const VisibleSelection& newSelectionPossiblyWithoutDirection, OptionSet<SetSelectionOption> options, CursorAlignOnScroll align, TextGranularity granularity)
{
bool closeTyping = options.contains(SetSelectionOption::CloseTyping);
bool shouldClearTypingStyle = options.contains(SetSelectionOption::ClearTypingStyle);
RefPtr document = m_document.get();
VisibleSelection newSelection = newSelectionPossiblyWithoutDirection;
if (shouldAlwaysUseDirectionalSelection(document.get()))
newSelection.setDirectionality(Directionality::Strong);
// <http://bugs.webkit.org/show_bug.cgi?id=23464>: Infinite recursion at FrameSelection::setSelection
// if document->frame() == m_document->frame() we can get into an infinite loop
if (RefPtr newSelectionDocument = newSelection.base().document()) {
if (RefPtr newSelectionFrame = newSelectionDocument->frame()) {
if (document && newSelectionFrame != document->frame() && newSelectionDocument != 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 && document && !options.contains(SetSelectionOption::DoNotNotifyEditorClients))
document->editor().selectionWillChange();
{
ScriptDisallowedScope::InMainThread scriptDisallowedScope;
if (newSelection.isOrphan()) {
ASSERT_NOT_REACHED();
clear();
return false;
}
if (!document || (!document->frame() && !newSelection.document())) {
setNodeFlags(m_selection, false);
m_selection = newSelection;
setNodeFlags(m_selection, true);
updateOrDisassociateLiveRange(options.contains(SetSelectionOption::MaintainLiveRange));
return false;
}
bool selectionEndpointsBelongToMultipleDocuments = newSelection.base().document() && !newSelection.document();
bool selectionIsInAnotherDocument = newSelection.document() && newSelection.document() != document.get();
bool selectionIsInDetachedDocument = newSelection.document() && !newSelection.document()->frame();
if (selectionEndpointsBelongToMultipleDocuments || selectionIsInAnotherDocument || selectionIsInDetachedDocument) {
clear();
return false;
}
ASSERT(document->frame());
if (closeTyping)
TypingCommand::closeTyping(*document);
if (shouldClearTypingStyle)
clearTypingStyle();
m_granularity = granularity;
setNodeFlags(m_selection, false);
m_selection = newSelection;
setNodeFlags(m_selection, true);
updateOrDisassociateLiveRange(options.contains(SetSelectionOption::MaintainLiveRange));
}
// Selection offsets should increase when LF is inserted before the caret in InsertLineBreakCommand. See <https://webkit.org/b/56061>.
// https://www.w3.org/TR/selection-api/#selectionchange-event
RefPtr textControl = enclosingTextFormControl(newSelection.start());
bool shouldScheduleSelectionChangeEvent = willMutateSelection;
if (textControl)
shouldScheduleSelectionChangeEvent = textControl->selectionChanged(options.contains(SetSelectionOption::FireSelectEvent));
if (!willMutateSelection)
return false;
setCaretRectNeedsUpdate();
if (!newSelection.isNone() && !(options & SetSelectionOption::DoNotSetFocus)) {
RefPtr oldFocusedElement = document->focusedElement();
setFocusedElementIfNeeded(options);
if (!document->frame())
return false;
// FIXME: Should not be needed.
if (document->focusedElement() != oldFocusedElement)
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();
if (!options.contains(SetSelectionOption::DoNotNotifyEditorClients))
document->editor().respondToChangedSelection(oldSelection, options);
if (shouldScheduleSelectionChangeEvent) {
if (textControl)
textControl->scheduleSelectionChangeEvent();
else if (!m_hasScheduledSelectionChangeEventOnDocument) {
m_hasScheduledSelectionChangeEventOnDocument = true;
document->eventLoop().queueTask(TaskSource::UserInteraction, [weakDocument = WeakPtr { document.get() }] {
if (RefPtr document = weakDocument.get()) {
document->selection().m_hasScheduledSelectionChangeEventOnDocument = false;
document->dispatchEvent(Event::create(eventNames().selectionchangeEvent, Event::CanBubble::No, Event::IsCancelable::No));
}
});
}
}
return true;
}
void FrameSelection::setSelection(const VisibleSelection& selection, OptionSet<SetSelectionOption> options, AXTextStateChangeIntent intent, CursorAlignOnScroll align, TextGranularity granularity)
{
LOG_WITH_STREAM(Selection, stream << "FrameSelection::setSelection " << selection);
RefPtr document = m_document.get();
if (!setSelectionWithoutUpdatingAppearance(selection, options, align, granularity))
return;
if (options & SetSelectionOption::RevealSelectionUpToMainFrame)
m_selectionRevealMode = SelectionRevealMode::RevealUpToMainFrame;
else if (options & SetSelectionOption::RevealSelection)
m_selectionRevealMode = SelectionRevealMode::Reveal;
else if (options & SetSelectionOption::DelegateMainFrameScroll)
m_selectionRevealMode = SelectionRevealMode::DelegateMainFrameScroll;
else
m_selectionRevealMode = SelectionRevealMode::DoNotReveal;
m_alwaysAlignCursorOnScrollWhenRevealingSelection = align == CursorAlignOnScroll::Always;
m_selectionRevealIntent = intent;
m_pendingSelectionUpdate = true;
document->scheduleContentRelevancyUpdate(ContentRelevancy::Selected);
if (document->hasPendingStyleRecalc())
return;
RefPtr frameView = document->view();
if (frameView && frameView->layoutContext().isLayoutPending())
return;
if (!(options & SetSelectionOption::IsUserTriggered))
return;
updateAndRevealSelection(intent, options.contains(SetSelectionOption::SmoothScroll) ? ScrollBehavior::Smooth : ScrollBehavior::Instant,
options.contains(SetSelectionOption::RevealSelectionBounds) ? RevealExtentOption::DoNotRevealExtent : RevealExtentOption::RevealExtent,
options.contains(SetSelectionOption::ForceCenterScroll) ? ForceCenterScroll::Yes : ForceCenterScroll::No,
options.contains(SetSelectionOption::OnlyAllowForwardScrolling) ? OnlyAllowForwardScrolling::Yes : OnlyAllowForwardScrolling::No);
if (options & SetSelectionOption::IsUserTriggered) {
if (auto* client = document->editor().client())
client->didEndUserTriggeredSelectionChanges();
}
}
void FrameSelection::updateSelectionAppearanceNow()
{
RefPtr document = m_document.get();
if (!document || !document->hasLivingRenderTree())
return;
#if ENABLE(TEXT_CARET)
document->updateLayoutIgnorePendingStylesheets();
#else
document->updateStyleIfNeeded();
#endif
if (m_pendingSelectionUpdate)
updateAppearance();
}
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, ForceCenterScroll forceCenterScroll, OnlyAllowForwardScrolling onlyAllowForwardScrolling)
{
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;
if (forceCenterScroll == ForceCenterScroll::Yes)
alignment = ScrollAlignment::alignCenterAlways;
revealSelection({ m_selectionRevealMode, alignment, revealExtent, scrollBehavior, onlyAllowForwardScrolling });
}
if (!m_document->editor().ignoreSelectionChanges())
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;
RefPtr element = dynamicDowncast<Element>(node);
return element && element->isShadowIncludingInclusiveAncestorOf(position.protectedAnchorNode().get());
}
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();
// It's important to avoid updating style or layout here, since we're in the middle of removing the node from the document.
clearCaretPositionWithoutUpdatingStyle();
}
void DragCaretController::clearCaretPositionWithoutUpdatingStyle()
{
if (RefPtr node = m_position.deepEquivalent().anchorNode())
invalidateCaretRect(node.get(), true);
m_position = { };
clearCaretRect();
}
static void setNodeContainsSelectionEndPoint(const Position& position, bool value)
{
// We use anchorNode instead of containerNode() because nodeWillBeRemoved must update position when anchored node is removed.
for (RefPtr currentNode = position.anchorNode(); currentNode; currentNode = currentNode->parentOrShadowHostNode()) {
if (currentNode->containsSelectionEndPoint() == value) {
#if ASSERT_ENABLED
for (RefPtr ancestor = currentNode; ancestor; ancestor = ancestor->parentOrShadowHostNode())
ASSERT(ancestor->containsSelectionEndPoint() == value);
#endif
break;
}
currentNode->setContainsSelectionEndPoint(value);
}
#if ASSERT_ENABLED
for (RefPtr ancestor = position.anchorNode(); ancestor; ancestor = ancestor->parentOrShadowHostNode())
ASSERT(ancestor->containsSelectionEndPoint() == value);
#endif
}
void FrameSelection::setNodeFlags(VisibleSelection& selection, bool value)
{
if (!m_document)
return; // Ignore local FrameSelection created in TypingCommand::deleteKeyPressed and TypingCommand::forwardDeleteKeyPressed.
#if ASSERT_ENABLED
for (RefPtr<Node> node = selection.document(); node; node = NodeTraversal::next(*node)) {
if (node->containsSelectionEndPoint()) {
for (Node* ancestor = node.get(); ancestor; ancestor = ancestor->parentOrShadowHostNode())
ASSERT(ancestor->containsSelectionEndPoint());
}
}
#endif
setNodeContainsSelectionEndPoint(selection.anchor(), value);
setNodeContainsSelectionEndPoint(selection.focus(), value);
setNodeContainsSelectionEndPoint(selection.base(), value);
setNodeContainsSelectionEndPoint(selection.extent(), value);
setNodeContainsSelectionEndPoint(selection.start(), value);
setNodeContainsSelectionEndPoint(selection.end(), value);
#if ASSERT_ENABLED
for (RefPtr<Node> node = selection.document(); node; node = NodeTraversal::next(*node)) {
if (node->containsSelectionEndPoint()) {
for (Node* ancestor = node.get(); ancestor; ancestor = ancestor->parentOrShadowHostNode())
ASSERT(ancestor->containsSelectionEndPoint());
}
}
#endif
}
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 (!node.isConnected())
return;
if (!node.containsSelectionEndPoint()) {
ASSERT(!removingNodeRemovesPosition(node, m_selection.anchor()));
ASSERT(!removingNodeRemovesPosition(node, m_selection.focus()));
ASSERT(!removingNodeRemovesPosition(node, m_selection.base()));
ASSERT(!removingNodeRemovesPosition(node, m_selection.extent()));
ASSERT(!removingNodeRemovesPosition(node, m_selection.start()));
ASSERT(!removingNodeRemovesPosition(node, m_selection.end()));
return;
}
respondToNodeModification(node, removingNodeRemovesPosition(node, m_selection.anchor()), removingNodeRemovesPosition(node, m_selection.focus()),
removingNodeRemovesPosition(node, m_selection.base()), removingNodeRemovesPosition(node, m_selection.extent()),
removingNodeRemovesPosition(node, m_selection.start()), removingNodeRemovesPosition(node, m_selection.end()));
if (node.contains(m_previousCaretNode.get())) [[unlikely]] {
m_previousCaretNode = m_selection.start().anchorNode();
setCaretRectNeedsUpdate();
}
}
void FrameSelection::respondToNodeModification(Node& node, bool anchorRemoved, bool focusRemoved, bool baseRemoved, bool extentRemoved, bool startRemoved, bool endRemoved)
{
bool clearRenderTreeSelection = false;
bool clearDOMTreeSelection = false;
if (anchorRemoved || focusRemoved) {
Position anchor = m_selection.anchor();
Position focus = m_selection.focus();
if (anchorRemoved)
updatePositionForNodeRemoval(anchor, node);
if (focusRemoved)
updatePositionForNodeRemoval(focus, node);
if (anchor.isNotNull() && focus.isNotNull()) {
setNodeFlags(m_selection, false);
m_selection.setWithoutValidation(anchor, focus);
setNodeFlags(m_selection, true);
} else
clearDOMTreeSelection = true;
clearRenderTreeSelection = true;
} 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()) {
setNodeFlags(m_selection, false);
if (m_selection.isBaseFirst())
m_selection.setWithoutValidation(start, end);
else
m_selection.setWithoutValidation(end, start);
setNodeFlags(m_selection, true);
} 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.
setNodeFlags(m_selection, false);
if (m_selection.isBaseFirst())
m_selection.setWithoutValidation(m_selection.start(), m_selection.end());
else
m_selection.setWithoutValidation(m_selection.end(), m_selection.start());
setNodeFlags(m_selection, true);
} else if (isRange()) {
if (auto range = m_selection.firstRange(); range && intersects<ComposedTree>(*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 (CheckedPtr 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(), { SetSelectionOption::DoNotSetFocus, SetSelectionOption::MaintainLiveRange });
}
static void updatePositionAfterAdoptingTextReplacement(Position& position, CharacterData& node, unsigned offset, unsigned oldLength, unsigned newLength)
{
if (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<unsigned>(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<unsigned>(position.offsetInContainerNode()) <= node.length());
}
void FrameSelection::textWasReplaced(CharacterData& node, unsigned offset, unsigned oldLength, unsigned newLength)
{
if (isNone() || !node.isConnected())
return;
Position anchor = m_selection.anchor();
Position focus = m_selection.focus();
Position base = m_selection.base();
Position extent = m_selection.extent();
Position start = m_selection.start();
Position end = m_selection.end();
updatePositionAfterAdoptingTextReplacement(anchor, node, offset, oldLength, newLength);
updatePositionAfterAdoptingTextReplacement(focus, node, offset, oldLength, newLength);
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 ((anchor != m_selection.anchor() || focus != m_selection.focus())
|| base != m_selection.base() || extent != m_selection.extent() || start != m_selection.start() || end != m_selection.end()) {
VisibleSelection newSelection;
newSelection.setWithoutValidation(anchor, focus);
setSelection(newSelection, { SetSelectionOption::DoNotSetFocus, SetSelectionOption::MaintainLiveRange });
}
}
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 startBox = startPosition.inlineBoxAndOffset().box;
auto endBox = endPosition.inlineBoxAndOffset().box;
if (startBox && endBox && startBox->direction() == endBox->direction())
return startBox->direction();
return directionOfEnclosingBlock();
}
static bool selectionIsOrphanedOrBelongsToWrongDocument(const VisibleSelection& selection, RefPtr<Document>&& document)
{
if (selection.isOrphan())
return true;
RefPtr documentOfSelection = selection.document();
return document && documentOfSelection && document != documentOfSelection;
}
void FrameSelection::willBeModified(Alteration alter, SelectionDirection direction)
{
if (alter != Alteration::Extend)
return;
Position start = m_selection.start();
Position end = m_selection.end();
bool baseIsStart = true;
if (m_selection.directionality() == Directionality::Strong) {
// Make base and extent match start and end so we extend the user-visible selection.
// This only matters for cases where base and extent 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;
}
}
setNodeFlags(m_selection, false);
if (baseIsStart) {
m_selection.setBase(start);
m_selection.setExtent(end);
} else {
m_selection.setBase(end);
m_selection.setExtent(start);
}
setNodeFlags(m_selection, true);
if (selectionIsOrphanedOrBelongsToWrongDocument(m_selection, m_document.get()))
clear();
}
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;
}
void FrameSelection::adjustSelectionExtentIfNeeded(VisiblePosition& extent, bool isForward, UserTriggered userTriggered)
{
if (userTriggered == UserTriggered::Yes) {
#if PLATFORM(IOS_FAMILY)
if (RefPtr document = this->document()) {
if (document->settings().visuallyContiguousBidiTextSelectionEnabled())
adjustVisibleExtentPreservingVisualContiguity(m_selection.base(), extent, isForward ? SelectionExtentMovement::Right : SelectionExtentMovement::Left);
}
#endif
}
if (RefPtr rootUserSelectAll = Position::rootUserSelectAllForNode(extent.deepEquivalent().anchorNode()))
extent = isForward ? positionAfterNode(rootUserSelectAll.get()).downstream(CanCrossEditingBoundary) : positionBeforeNode(rootUserSelectAll.get()).upstream(CanCrossEditingBoundary);
}
VisiblePosition FrameSelection::modifyExtendingRight(TextGranularity granularity, UserTriggered userTriggered)
{
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, userTriggered);
else
pos = modifyExtendingBackward(granularity, userTriggered);
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, userTriggered);
break;
case TextGranularity::DocumentGranularity:
ASSERT_NOT_REACHED();
break;
}
adjustSelectionExtentIfNeeded(pos, directionOfEnclosingBlock() == TextDirection::LTR, userTriggered);
return pos;
}
VisiblePosition FrameSelection::modifyExtendingForward(TextGranularity granularity, UserTriggered userTriggered)
{
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(PositionType::Extent));
break;
case TextGranularity::ParagraphGranularity:
pos = nextParagraphPosition(pos, lineDirectionPointForBlockDirectionNavigation(PositionType::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;
}
adjustSelectionExtentIfNeeded(pos, directionOfEnclosingBlock() == TextDirection::LTR, userTriggered);
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(PositionType::Start));
break;
}
case TextGranularity::ParagraphGranularity:
pos = nextParagraphPosition(currentPosition, lineDirectionPointForBlockDirectionNavigation(PositionType::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, UserTriggered userTriggered)
{
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, userTriggered);
else
pos = modifyExtendingForward(granularity, userTriggered);
break;
case TextGranularity::SentenceGranularity:
case TextGranularity::LineGranularity:
case TextGranularity::ParagraphGranularity:
case TextGranularity::SentenceBoundary:
case TextGranularity::ParagraphBoundary:
case TextGranularity::DocumentBoundary:
pos = modifyExtendingBackward(granularity, userTriggered);
break;
case TextGranularity::DocumentGranularity:
ASSERT_NOT_REACHED();
break;
}
adjustSelectionExtentIfNeeded(pos, directionOfEnclosingBlock() == TextDirection::RTL, userTriggered);
return pos;
}
VisiblePosition FrameSelection::modifyExtendingBackward(TextGranularity granularity, UserTriggered userTriggered)
{
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(PositionType::Extent));
break;
case TextGranularity::ParagraphGranularity:
pos = previousParagraphPosition(pos, lineDirectionPointForBlockDirectionNavigation(PositionType::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;
}
adjustSelectionExtentIfNeeded(pos, directionOfEnclosingBlock() == TextDirection::RTL, userTriggered);
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(PositionType::Start));
break;
case TextGranularity::ParagraphGranularity:
pos = previousParagraphPosition(currentPosition, lineDirectionPointForBlockDirectionNavigation(PositionType::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(Alteration alter, SelectionDirection direction, TextGranularity granularity)
{
AXTextStateChangeIntent intent = AXTextStateChangeIntent();
bool flip = false;
if (alter == FrameSelection::Alteration::Move) {
intent.type = AXTextStateChangeType::SelectionMove;
flip = isRange() && directionOfSelection() == TextDirection::RTL;
} else
intent.type = AXTextStateChangeType::SelectionExtend;
switch (granularity) {
case TextGranularity::CharacterGranularity:
intent.selection.granularity = AXTextSelectionGranularity::Character;
break;
case TextGranularity::WordGranularity:
intent.selection.granularity = AXTextSelectionGranularity::Word;
break;
case TextGranularity::SentenceGranularity:
case TextGranularity::SentenceBoundary:
intent.selection.granularity = AXTextSelectionGranularity::Sentence;
break;
case TextGranularity::LineGranularity:
case TextGranularity::LineBoundary:
intent.selection.granularity = AXTextSelectionGranularity::Line;
break;
case TextGranularity::ParagraphGranularity:
case TextGranularity::ParagraphBoundary:
intent.selection.granularity = AXTextSelectionGranularity::Paragraph;
break;
case TextGranularity::DocumentGranularity:
case TextGranularity::DocumentBoundary:
intent.selection.granularity = AXTextSelectionGranularity::Document;
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 ? AXTextSelectionDirection::Beginning : AXTextSelectionDirection::End;
else
intent.selection.direction = flip ? AXTextSelectionDirection::Previous : AXTextSelectionDirection::Next;
break;
case SelectionDirection::Left:
case SelectionDirection::Backward:
if (boundary)
intent.selection.direction = flip ? AXTextSelectionDirection::End : AXTextSelectionDirection::Beginning;
else
intent.selection.direction = flip ? AXTextSelectionDirection::Next : AXTextSelectionDirection::Previous;
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 = AXTextSelectionDirection::Unknown;
switch (direction) {
case SelectionDirection::Forward:
intentDirection = AXTextSelectionDirection::Next;
break;
case SelectionDirection::Right:
intentDirection = AXTextSelectionDirection::Next;
break;
case SelectionDirection::Backward:
intentDirection = AXTextSelectionDirection::Previous;
break;
case SelectionDirection::Left:
intentDirection = AXTextSelectionDirection::Previous;
break;
}
AXTextSelectionGranularity intentGranularity = AXTextSelectionGranularity::Unknown;
switch (granularity) {
case TextGranularity::CharacterGranularity:
intentGranularity = AXTextSelectionGranularity::Character;
break;
case TextGranularity::WordGranularity:
intentGranularity = AXTextSelectionGranularity::Word;
break;
case TextGranularity::SentenceGranularity:
case TextGranularity::SentenceBoundary: // FIXME: Boundary should affect direction.
intentGranularity = AXTextSelectionGranularity::Sentence;
break;
case TextGranularity::LineGranularity:
intentGranularity = AXTextSelectionGranularity::Line;
break;
case TextGranularity::ParagraphGranularity:
case TextGranularity::ParagraphBoundary: // FIXME: Boundary should affect direction.
intentGranularity = AXTextSelectionGranularity::Paragraph;
break;
case TextGranularity::DocumentGranularity:
case TextGranularity::DocumentBoundary: // FIXME: Boundary should affect direction.
intentGranularity = AXTextSelectionGranularity::Document;
break;
case TextGranularity::LineBoundary:
intentGranularity = AXTextSelectionGranularity::Line;
switch (direction) {
case SelectionDirection::Forward:
intentDirection = AXTextSelectionDirection::End;
break;
case SelectionDirection::Right:
intentDirection = AXTextSelectionDirection::End;
break;
case SelectionDirection::Backward:
intentDirection = AXTextSelectionDirection::Beginning;
break;
case SelectionDirection::Left:
intentDirection = AXTextSelectionDirection::Beginning;
break;
}
break;
}
return { intentDirection, intentGranularity, false };
}
bool FrameSelection::modify(Alteration alter, SelectionDirection direction, TextGranularity granularity, UserTriggered userTriggered)
{
if (userTriggered == UserTriggered::Yes) {
auto trialFrameSelection = makeUniqueRef<FrameSelection>();
trialFrameSelection->setSelection(m_selection);
trialFrameSelection->modify(alter, direction, granularity, UserTriggered::No);
bool change = shouldChangeSelection(trialFrameSelection->selection());
if (!change)
return false;
if (trialFrameSelection->selection().isRange() && m_selection.isCaret() && !dispatchSelectStart())
return false;
}
willBeModified(alter, direction);
// Before modifying selection, update layout and disable post resolution callbacks.
// That way, unaverted tree changes are avoided while browsing the document.
auto selectionDocument = m_selection.document();
if (!selectionDocument)
return false;
selectionDocument->updateLayoutIgnorePendingStylesheets();
Style::PostResolutionCallbackDisabler disabler(*selectionDocument);
bool reachedBoundary = false;
bool wasRange = m_selection.isRange();
Position originalStartPosition = m_selection.start();
VisiblePosition position;
switch (direction) {
case SelectionDirection::Right:
if (alter == Alteration::Move)
position = modifyMovingRight(granularity, &reachedBoundary);
else
position = modifyExtendingRight(granularity, userTriggered);
break;
case SelectionDirection::Forward:
if (alter == Alteration::Extend)
position = modifyExtendingForward(granularity, userTriggered);
else
position = modifyMovingForward(granularity, &reachedBoundary);
break;
case SelectionDirection::Left:
if (alter == Alteration::Move)
position = modifyMovingLeft(granularity, &reachedBoundary);
else
position = modifyExtendingLeft(granularity, userTriggered);
break;
case SelectionDirection::Backward:
if (alter == Alteration::Extend)
position = modifyExtendingBackward(granularity, userTriggered);
else
position = modifyMovingBackward(granularity, &reachedBoundary);
break;
}
if (reachedBoundary && !isRange() && userTriggered == UserTriggered::Yes && m_document && AXObjectCache::accessibilityEnabled()) {
notifyAccessibilityForSelectionChange({ AXTextStateChangeType::SelectionBoundary, textSelectionWithDirectionAndGranularity(direction, granularity) });
return true;
}
if (position.isNull())
return false;
if (m_document && m_document->settings().spatialNavigationEnabled()) {
if (!wasRange && alter == Alteration::Move && 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(PositionType::Start);
m_selection.setDirectionality((shouldAlwaysUseDirectionalSelection(m_document.get()) || alter == Alteration::Extend)
? Directionality::Strong : Directionality::None);
switch (alter) {
case Alteration::Move:
moveTo(position, userTriggered);
break;
case Alteration::Extend:
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::Yes)
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(Alteration alter, unsigned verticalDistance, VerticalDirection direction, UserTriggered userTriggered, CursorAlignOnScroll align)
{
if (!verticalDistance)
return false;
if (userTriggered == UserTriggered::Yes) {
auto trialFrameSelection = makeUniqueRef<FrameSelection>();
trialFrameSelection->setSelection(m_selection);
trialFrameSelection->modify(alter, verticalDistance, direction, UserTriggered::No);
bool change = shouldChangeSelection(trialFrameSelection->selection());
if (!change)
return false;
}
willBeModified(alter, direction == VerticalDirection::Up ? SelectionDirection::Backward : SelectionDirection::Forward);
VisiblePosition pos;
LayoutUnit xPos;
switch (alter) {
case Alteration::Move:
pos = VisiblePosition(direction == VerticalDirection::Up ? m_selection.start() : m_selection.end(), m_selection.affinity());
xPos = lineDirectionPointForBlockDirectionNavigation(direction == VerticalDirection::Up ? PositionType::Start : PositionType::End);
m_selection.setAffinity(direction == VerticalDirection::Up ? Affinity::Upstream : Affinity::Downstream);
break;
case Alteration::Extend:
pos = VisiblePosition(m_selection.extent(), m_selection.affinity());
xPos = lineDirectionPointForBlockDirectionNavigation(PositionType::Extent);
m_selection.setAffinity(Affinity::Downstream);
break;
}
int startY;
if (!absoluteCaretY(pos, startY))
return false;
if (direction == VerticalDirection::Up)
startY = -startY;
int lastY = startY;
VisiblePosition result;
VisiblePosition next;
for (VisiblePosition p = pos; ; p = next) {
if (direction == VerticalDirection::Up)
next = previousLinePosition(p, xPos);
else
next = nextLinePosition(p, xPos);
if (next.isNull() || next == p)
break;
int nextY;
if (!absoluteCaretY(next, nextY))
break;
if (direction == VerticalDirection::Up)
nextY = -nextY;
if (nextY - startY > static_cast<int>(verticalDistance))
break;
if (nextY >= lastY) {
lastY = nextY;
result = next;
}
}
if (result.isNull())
return false;
switch (alter) {
case Alteration::Move:
moveTo(result, userTriggered, align);
break;
case Alteration::Extend:
setExtent(result, userTriggered);
break;
}
if (userTriggered == UserTriggered::Yes)
m_granularity = TextGranularity::CharacterGranularity;
m_selection.setDirectionality((shouldAlwaysUseDirectionalSelection(m_document.get()) || alter == Alteration::Extend)
? Directionality::Strong : Directionality::None);
return true;
}
LayoutUnit FrameSelection::lineDirectionPointForBlockDirectionNavigation(PositionType type)
{
if (isNone())
return 0;
// FIXME: Can we use visibleStart/End/Extent?
Position position;
switch (type) {
case PositionType::Start:
position = m_selection.start();
break;
case PositionType::End:
position = m_selection.end();
break;
case PositionType::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)
caretAnimator().stop();
#endif
if (auto* view = m_document->renderView())
view->selection().clear();
setSelectionWithoutUpdatingAppearance(VisibleSelection(), defaultSetSelectionOptions() | SetSelectionOption::DoNotNotifyEditorClients,
CursorAlignOnScroll::IfNeeded, TextGranularity::CharacterGranularity);
m_previousCaretNode = nullptr;
m_typingStyle = nullptr;
m_originalBase = { };
}
void FrameSelection::setStart(const VisiblePosition& position, UserTriggered trigger)
{
if (m_selection.isBaseFirst())
setBase(position, trigger);
else
setExtent(position, trigger);
}
void FrameSelection::setEnd(const VisiblePosition& position, UserTriggered trigger)
{
if (m_selection.isBaseFirst())
setExtent(position, trigger);
else
setBase(position, trigger);
}
void FrameSelection::setBase(const VisiblePosition& position, UserTriggered userTriggered)
{
setSelection(VisibleSelection(position.deepEquivalent(), m_selection.extent(), position.affinity(), Directionality::Strong), defaultSetSelectionOptions(userTriggered));
}
void FrameSelection::setExtent(const VisiblePosition& position, UserTriggered userTriggered)
{
setSelection(VisibleSelection(m_selection.base(), position.deepEquivalent(), position.affinity(), Directionality::Strong), defaultSetSelectionOptions(userTriggered));
}
void FrameSelection::setBase(const Position& position, Affinity affinity, UserTriggered userTriggered)
{
setSelection(VisibleSelection(position, m_selection.extent(), affinity, Directionality::Strong), defaultSetSelectionOptions(userTriggered));
}
void FrameSelection::setExtent(const Position& position, Affinity affinity, UserTriggered userTriggered)
{
setSelection(VisibleSelection(m_selection.base(), position, affinity, Directionality::Strong), 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
{
if (m_position.isNull())
return nullptr;
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();
updateSelectionAppearanceNow();
recomputeCaretRect();
if (insideFixed)
*insideFixed = m_caretInsidePositionFixed;
return m_absCaretBounds;
}
static LayoutBoxExtent computeOutsetFromInnerOuterRect(const LayoutRect& innerRect, const LayoutRect& outerRect)
{
LayoutBoxExtent result;
result.setLeft(std::max<LayoutUnit>(0, innerRect.x() - outerRect.x()));
result.setTop(std::max<LayoutUnit>(0, innerRect.y() - outerRect.y()));
result.setRight(std::max<LayoutUnit>(0, outerRect.width() - innerRect.width()));
result.setBottom(std::max<LayoutUnit>(0, outerRect.height() - innerRect.height()));
return result;
}
static void repaintCaretForLocalRect(Node* node, const LayoutRect& rect, CaretAnimator* caretAnimator)
{
if (CheckedPtr caretPainter = rendererForCaretPainting(node)) {
LayoutRect adjustedRect = caretAnimator ? caretAnimator->caretRepaintRectForLocalRect(rect) : rect;
if (adjustedRect == rect)
caretPainter->repaintRectangle(rect);
else
caretPainter->repaintRectangle(rect, RenderObject::ClipRepaintToLayer::No, RenderObject::ForceRepaint::Yes, computeOutsetFromInnerOuterRect(rect, adjustedRect));
}
}
bool FrameSelection::recomputeCaretRect()
{
if (!shouldUpdateCaretRect())
return false;
RefPtr document = m_document.get();
if (!document)
return false;
if (!document->view())
return false;
LayoutRect oldRect = localCaretRectWithoutUpdate();
RefPtr<Node> caretNode = m_previousCaretNode;
if (shouldUpdateCaretRect()) {
if (!isNonOrphanedCaret(m_selection))
clearCaretRect();
else {
VisiblePosition visibleStart = m_selection.visibleStart();
if (updateCaretRect(*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 (CheckedPtr view = document->renderView()) {
bool previousOrNewCaretNodeIsContentEditable = m_selection.isContentEditable() || (m_previousCaretNode && m_previousCaretNode->isContentEditable());
if (shouldRepaintCaret(view.get(), previousOrNewCaretNodeIsContentEditable)) {
if (m_previousCaretNode)
repaintCaretForLocalRect(m_previousCaretNode.get(), oldRect, m_caretAnimator.ptr());
m_previousCaretNode = caretNode;
repaintCaretForLocalRect(caretNode.get(), newRect, m_caretAnimator.ptr());
}
}
#endif
return true;
}
bool CaretBase::shouldRepaintCaret(const RenderView* view, bool isContentEditable) const
{
ASSERT(view);
bool caretBrowsing = view->frameView().frame().settings().caretBrowsingEnabled(); // The frame where the selection started.
return (caretBrowsing || isContentEditable);
}
void FrameSelection::invalidateCaretRect()
{
if (!isCaret())
return;
CaretBase::invalidateCaretRect(m_selection.start().deprecatedNode(), recomputeCaretRect(), m_caretAnimator.ptr());
}
void CaretBase::invalidateCaretRect(Node* node, bool caretRectChanged, CaretAnimator* caretAnimator)
{
// 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 (CheckedPtr view = node->document().renderView()) {
if (shouldRepaintCaret(view.get(), isEditableNode(*node)))
repaintCaretForLocalRect(node, localCaretRectWithoutUpdate(), caretAnimator);
}
}
void FrameSelection::paintCaret(GraphicsContext& context, const LayoutPoint& paintOffset)
{
if (m_selection.isCaret() && m_selection.start().deprecatedNode())
CaretBase::paintCaret(*m_selection.start().deprecatedNode(), context, paintOffset, m_caretAnimator.ptr());
}
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);
if (elementStyle.caretColor().isAuto())
return { };
return elementStyle.caretColorResolvingCurrentColor();
#elif HAVE(REDESIGNED_TEXT_CURSOR)
#if HAVE(APP_ACCENT_COLORS) && PLATFORM(MAC)
auto appUsesCustomAccentColor = node && node->document().page() && node->document().page()->appUsesCustomAccentColor();
#else
auto appUsesCustomAccentColor = false;
#endif
if (elementStyle.caretColor().isAuto() && (!elementStyle.hasExplicitlySetColor() || appUsesCustomAccentColor)) {
#if PLATFORM(MAC)
auto cssColorValue = CSSValueAppleSystemControlAccent;
#else
auto cssColorValue = CSSValueAppleSystemBlue;
#endif
auto styleColorOptions = node->protectedDocument()->styleColorOptions(&elementStyle);
auto systemAccentColor = RenderTheme::singleton().systemColor(cssColorValue, styleColorOptions | StyleColorOptions::UseSystemAppearance);
Style::ColorResolver colorResolver { elementStyle };
return colorResolver.colorApplyingColorFilter(systemAccentColor);
}
return elementStyle.visitedDependentCaretColorApplyingColorFilter();
#else
RefPtr parentElement = node ? node->parentElement() : nullptr;
auto* parentStyle = parentElement && parentElement->renderer() ? &parentElement->renderer()->style() : nullptr;
// CSS value "auto" is treated as an invalid color.
if (elementStyle.caretColor().isAuto() && parentStyle) {
auto parentBackgroundColor = parentStyle->visitedDependentBackgroundColorApplyingColorFilter();
auto elementBackgroundColor = elementStyle.visitedDependentBackgroundColorApplyingColorFilter();
auto disappearsIntoBackground = blendSourceOver(parentBackgroundColor, elementBackgroundColor) == parentBackgroundColor;
if (disappearsIntoBackground)
return parentStyle->visitedDependentCaretColorApplyingColorFilter();
}
return elementStyle.visitedDependentCaretColorApplyingColorFilter();
#endif
}
void CaretBase::paintCaret(const Node& node, GraphicsContext& context, const LayoutPoint& paintOffset, CaretAnimator* caretAnimator) const
{
#if ENABLE(TEXT_CARET)
auto caretPresentationProperties = caretAnimator ? caretAnimator->presentationProperties() : CaretAnimator::PresentationProperties();
if (m_caretVisibility == CaretVisibility::Hidden || caretPresentationProperties.blinkState == CaretAnimator::PresentationProperties::BlinkState::Off)
return;
auto caret = localCaretRectWithoutUpdate();
if (CheckedPtr renderer = rendererForCaretPainting(&node))
renderer->flipForWritingMode(caret);
caret.moveBy(paintOffset);
if (caret.isEmpty())
return;
Color caretColor = Color::black;
RefPtr element = dynamicDowncast<Element>(node);
if (!element)
element = node.parentElement();
if (element && element->renderer())
caretColor = CaretBase::computeCaretColor(element->renderer()->style(), &node);
auto pixelSnappedCaretRect = snapRectToDevicePixels(caret, node.document().deviceScaleFactor());
if (caretAnimator)
caretAnimator->paint(context, pixelSnappedCaretRect, caretColor, paintOffset);
else
context.fillRect(pixelSnappedCaretRect, caretColor);
#else
UNUSED_PARAM(node);
UNUSED_PARAM(context);
UNUSED_PARAM(paintOffset);
UNUSED_PARAM(caretAnimator);
#endif
}
void FrameSelection::setCaretBlinkingSuspended(bool suspended)
{
caretAnimator().setBlinkingSuspended(suspended);
}
bool FrameSelection::isCaretBlinkingSuspended() const
{
return caretAnimator().isBlinkingSuspended();
}
void FrameSelection::caretAnimationDidUpdate(CaretAnimator&)
{
invalidateCaretRect();
}
#if ENABLE(ACCESSIBILITY_NON_BLINKING_CURSOR)
void FrameSelection::setPrefersNonBlinkingCursor(bool enabled)
{
caretAnimator().setPrefersNonBlinkingCursor(enabled);
}
#endif
#if PLATFORM(MAC)
void FrameSelection::caretAnimatorInvalidated(CaretAnimatorType caretType)
{
m_caretAnimator = createCaretAnimator(this, caretType);
caretAnimationDidUpdate(m_caretAnimator);
updateAppearance();
}
#endif
Document* FrameSelection::document()
{
return m_document.get();
}
Node* FrameSelection::caretNode()
{
return selection().visibleStart().deepEquivalent().deprecatedNode();
}
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;
RefPtr document = m_document.get();
if (!document)
return false;
HitTestResult result(point);
document->hitTest(HitTestRequest(), result);
RefPtr innerNode = result.innerNode();
if (!innerNode || !innerNode->renderer())
return false;
if (ImageOverlay::isInsideOverlay(*range) && ImageOverlay::isInsideOverlay(*innerNode)) {
for (auto quad : RenderObject::absoluteTextQuads(*range, { RenderObject::BoundingRectBehavior::UseSelectionHeight })) {
if (!quad.isEmpty() && quad.containsPoint(point))
return true;
}
return false;
}
return WebCore::contains<ComposedTree>(*range, makeBoundaryPoint(innerNode->renderer()->visiblePositionForPoint(result.localPoint(), HitTestSource::User)));
}
// 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.
RefPtr document = m_document.get();
if (!document)
return;
RefPtr frame { document->frame() };
if (!frame)
return;
RefPtr parent { dynamicDowncast<LocalFrame>(frame->tree().parent()) };
if (!parent)
return;
RefPtr page = 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 <iframe> or <frame> (or even <object>) element in the parent frame.
RefPtr ownerElement { document->ownerElement() };
if (!ownerElement)
return;
RefPtr ownerElementParent { ownerElement->parentNode() };
if (!ownerElementParent)
return;
// This method's purpose is it to make it easier to select iframes (in order to delete them). Don't do anything if the iframe isn't deletable.
if (!ownerElementParent->hasEditableStyle())
return;
// Create compute positions before and after the element.
unsigned ownerElementNodeIndex = ownerElement->computeNodeIndex();
VisiblePosition beforeOwnerElement(VisiblePosition(Position(ownerElementParent.get(), ownerElementNodeIndex, Position::PositionIsOffsetInAnchor)));
VisiblePosition afterOwnerElement(VisiblePosition(Position(ownerElementParent.get(), ownerElementNodeIndex + 1, Position::PositionIsOffsetInAnchor), Affinity::Upstream));
// Focus on the parent frame, and then select from before this element to after.
VisibleSelection newSelection(beforeOwnerElement, afterOwnerElement);
if (parent->selection().shouldChangeSelection(newSelection)) {
page->focusController().setFocusedFrame(parent.get());
// Previous focus can trigger DOM events, ensure the selection did not become orphan.
if (newSelection.isOrphan())
parent->selection().clear();
else
parent->selection().setSelection(newSelection);
}
}
void FrameSelection::selectAll()
{
RefPtr focusedElement = m_document->focusedElement();
if (RefPtr selectElement = dynamicDowncast<HTMLSelectElement>(focusedElement)) {
if (selectElement->canSelectAll()) {
selectElement->selectAll();
return;
}
}
RefPtr<Node> root;
RefPtr<Node> selectStartTarget;
if (m_selection.isContentEditable()) {
root = highestEditableRoot(m_selection.start());
if (RefPtr shadowRoot = m_selection.nonBoundaryShadowTreeRootNode())
selectStartTarget = shadowRoot->shadowHost();
else
selectStartTarget = root.get();
} else {
if (m_selection.isNone() && focusedElement) {
if (focusedElement->isTextField()) {
downcast<HTMLTextFormControlElement>(*focusedElement).select();
return;
}
root = focusedElement->nonBoundaryShadowTreeRootNode();
} else
root = m_selection.nonBoundaryShadowTreeRootNode();
if (root)
selectStartTarget = root->shadowHost();
else {
root = m_document->documentElement();
selectStartTarget = m_document->bodyOrFrameset();
}
}
if (!root)
return;
if (selectStartTarget) {
auto event = Event::create(eventNames().selectstartEvent, Event::CanBubble::Yes, Event::IsCancelable::Yes);
selectStartTarget->dispatchEvent(event);
if (event->defaultPrevented())
return;
}
VisibleSelection newSelection(VisibleSelection::selectionFromContentsOfNode(root.get()));
if (!newSelection.isOrphan() && shouldChangeSelection(newSelection)) {
AXTextStateChangeIntent intent(AXTextStateChangeType::SelectionExtend, AXTextSelection { AXTextSelectionDirection::Discontiguous, AXTextSelectionGranularity::All, false });
setSelection(newSelection, defaultSetSelectionOptions() | SetSelectionOption::FireSelectEvent, intent);
}
}
bool FrameSelection::setSelectedRange(const std::optional<SimpleRange>& range, Affinity affinity, ShouldCloseTyping closeTyping, UserTriggered userTriggered)
{
if (!range)
return false;
if (&range->start.document() != &range->end.document())
return false;
VisibleSelection newSelection(*range, affinity);
#if PLATFORM(IOS_FAMILY)
// FIXME: Why do we need this check only in iOS?
if (newSelection.isNone())
return false;
#endif
OptionSet<SetSelectionOption> selectionOptions { SetSelectionOption::ClearTypingStyle };
if (closeTyping == ShouldCloseTyping::Yes)
selectionOptions.add(SetSelectionOption::CloseTyping);
if (userTriggered == UserTriggered::Yes) {
auto trialFrameSelection = makeUniqueRef<FrameSelection>();
trialFrameSelection->setSelection(newSelection, selectionOptions);
if (!shouldChangeSelection(trialFrameSelection->selection()))
return false;
selectionOptions.add(SetSelectionOption::IsUserTriggered);
}
setSelection(newSelection, selectionOptions);
return true;
}
void FrameSelection::focusedOrActiveStateChanged()
{
bool activeAndFocused = isFocusedAndActive();
RefPtr document = m_document.get();
document->updateStyleIfNeeded();
#if USE(UIKIT_EDITING)
// Caret blinking (blinks | does not blink)
if (activeAndFocused) {
setSelectionFromNone();
removeCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::IsNotFocusedOrActive);
} else
addCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::IsNotFocusedOrActive);
#else
// Because RenderObject::selectionBackgroundColor() and
// RenderObject::selectionForegroundColor() check if the frame is active,
// we have to update places those colors were painted.
if (CheckedPtr view = document->renderView())
view->selection().repaint();
// Caret appears in the active frame.
if (activeAndFocused) {
setSelectionFromNone();
removeCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::IsNotFocusedOrActive);
} else
addCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason::IsNotFocusedOrActive);
#endif
}
static Vector<Style::PseudoClassChangeInvalidation> invalidateFocusedElementAndShadowIncludingAncestors(Element* focusedElement, bool activeAndFocused)
{
Vector<Style::PseudoClassChangeInvalidation> invalidations;
for (RefPtr element = focusedElement; element; element = element->shadowHost()) {
invalidations.append({ *element, { { CSSSelector::PseudoClass::Focus, activeAndFocused }, { CSSSelector::PseudoClass::FocusVisible, activeAndFocused } } });
for (Ref lineage : lineageOfType<Element>(*element))
invalidations.append({ lineage, CSSSelector::PseudoClass::FocusWithin, activeAndFocused });
}
return invalidations;
}
void FrameSelection::pageActivationChanged()
{
bool isActive = isPageActive(m_document.get());
RefPtr focusedElement = m_document->focusedElement();
{
auto invalidations = invalidateFocusedElementAndShadowIncludingAncestors(focusedElement.get(), m_focused && isActive);
m_isActive = isActive;
}
focusedOrActiveStateChanged();
}
void FrameSelection::setFocused(bool isFocused)
{
if (m_focused == isFocused)
return;
bool isActive = isPageActive(m_document.get());
RefPtr focusedElement = m_document->focusedElement();
{
auto invalidations = invalidateFocusedElementAndShadowIncludingAncestors(focusedElement.get(), isFocused && isActive);
m_focused = isFocused;
m_isActive = isActive;
}
focusedOrActiveStateChanged();
}
bool FrameSelection::isFocusedAndActive() const
{
return m_focused && m_document->page() && m_document->page()->focusController().isActive();
}
#if ENABLE(TEXT_CARET)
inline static bool shouldStopBlinkingDueToTypingCommand(Document* document)
{
return document->editor().lastEditCommand() && document->editor().lastEditCommand()->shouldStopCaretBlinking();
}
#endif
void FrameSelection::updateAppearance()
{
#if PLATFORM(IOS_FAMILY)
if (!m_updateAppearanceEnabled)
return;
#endif
// Paint a block cursor instead of a caret in overtype mode unless the caret is at the end of a line (in this case
// the FrameSelection will paint a blinking caret as usual).
VisibleSelection oldSelection = selection();
RefPtr document = m_document.get();
#if ENABLE(TEXT_CARET)
bool paintBlockCursor = m_shouldShowBlockCursor && m_selection.isCaret() && !isLogicalEndOfLine(m_selection.visibleEnd());
bool caretRectChangedOrCleared = recomputeCaretRect();
bool caretBrowsing = document->settings().caretBrowsingEnabled();
bool shouldBlink = !paintBlockCursor && caretIsVisible() && isCaret() && (oldSelection.isContentEditable() || caretBrowsing);
// If the caret moved, stop the blink timer so we can restart with a
// black caret in the new location.
if (caretRectChangedOrCleared || !shouldBlink || shouldStopBlinkingDueToTypingCommand(document.get()))
caretAnimator().stop(CaretAnimatorStopReason::CaretRectChanged);
// Start blinking with a black caret. Be sure not to restart if we're
// already blinking in the right location.
if (shouldBlink && !caretAnimator().isActive()) {
if (document && document->window())
caretAnimator().start();
caretAnimator().setVisible(true);
}
#endif
// Construct a new VisibleSolution, since m_selection is not necessarily valid, and the following steps
// assume a valid selection. See <https://bugs.webkit.org/show_bug.cgi?id=69563> and <rdar://problem/10232866>.
#if ENABLE(TEXT_CARET)
VisiblePosition endVisiblePosition = paintBlockCursor ? modifyExtendingForward(TextGranularity::CharacterGranularity, UserTriggered::No) : oldSelection.visibleEnd();
VisibleSelection selection(oldSelection.visibleStart(), endVisiblePosition);
#else
VisibleSelection selection(oldSelection.visibleStart(), oldSelection.visibleEnd());
#endif
{
ScriptDisallowedScope::InMainThread scriptDisallowedScope;
CheckedPtr view = document->renderView();
if (!view)
return;
if (!selection.isRange()) {
view->selection().clear();
return;
}
}
// Use the rightmost candidate for the start of the selection, and the leftmost candidate for the end of the selection.
// Example: foo <a>bar</a>. Imagine that a line wrap occurs after 'foo', and that 'bar' is selected. If we pass [foo, 3]
// as the start of the selection, the selection painting code will think that content on the line containing 'foo' is selected
// and will fill the gap before 'bar'.
Position startPos = selection.start();
Position candidate = startPos.downstream();
if (candidate.isCandidate())
startPos = candidate;
Position endPos = selection.end();
candidate = endPos.upstream();
if (candidate.isCandidate())
endPos = candidate;
// We can get into a state where the selection endpoints map to the same VisiblePosition when a selection is deleted
// because we don't yet notify the FrameSelection of text removal.
if (CheckedPtr view = document->renderView(); startPos.isNotNull() && endPos.isNotNull() && selection.visibleStart() != selection.visibleEnd()) {
RenderObject* startRenderer = startPos.deprecatedNode()->renderer();
int startOffset = startPos.deprecatedEditingOffset();
RenderObject* endRenderer = endPos.deprecatedNode()->renderer();
int endOffset = endPos.deprecatedEditingOffset();
ASSERT(startOffset >= 0 && endOffset >= 0);
view->selection().set({ startRenderer, endRenderer, static_cast<unsigned>(startOffset), static_cast<unsigned>(endOffset) });
}
}
void FrameSelection::addCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason reason, ShouldUpdateAppearance doAppearanceUpdate)
{
m_caretVisibilitySuppressionReasons.add(reason);
updateCaretVisibility(doAppearanceUpdate);
}
void FrameSelection::removeCaretVisibilitySuppressionReason(CaretVisibilitySuppressionReason reason, ShouldUpdateAppearance doAppearanceUpdate)
{
m_caretVisibilitySuppressionReasons.remove(reason);
updateCaretVisibility(doAppearanceUpdate);
}
void FrameSelection::updateCaretVisibility(ShouldUpdateAppearance doAppearanceUpdate)
{
auto visibility = m_caretVisibilitySuppressionReasons.isEmpty() ? CaretVisibility::Visible : CaretVisibility::Hidden;
if (caretVisibility() == visibility)
return;
#if ENABLE(TEXT_CARET)
caretAnimator().setVisible(false);
CaretBase::setCaretVisibility(visibility);
#endif
if (doAppearanceUpdate == ShouldUpdateAppearance::Yes)
m_pendingSelectionUpdate = true;
}
// Helper function that tells whether a particular node is an element that has an entire
// Frame and FrameView, a <frame>, <iframe>, or <object>.
static bool isFrameElement(const Node& node)
{
RefPtr renderer = dynamicDowncast<RenderWidget>(node.renderer());
if (!renderer)
return false;
RefPtr widget = renderer->widget();
return widget && widget->isLocalFrameView();
}
void FrameSelection::setFocusedElementIfNeeded(OptionSet<SetSelectionOption> options)
{
if (isNone() || !isFocused())
return;
RefPtr document = m_document.get();
bool caretBrowsing = document->settings().caretBrowsingEnabled();
if (caretBrowsing) {
if (RefPtr anchor = enclosingAnchorElement(m_selection.base())) {
CheckedRef focusController { document->page()->focusController() };
focusController->setFocusedElement(anchor.get(), document->protectedFrame().get());
return;
}
}
if (RefPtr target = m_selection.rootEditableElement()) {
// Walk up the DOM tree to search for an element to focus.
while (target) {
// We don't want to set focus on a subframe when selecting in a parent frame,
// so add the !isFrameElement check here. There's probably a better way to make this
// work in the long term, but this is the safest fix at this time.
if (target->isMouseFocusable() && !isFrameElement(*target)) {
FocusOptions focusOptions;
if (options & SetSelectionOption::ForBindings)
focusOptions.trigger = FocusTrigger::Bindings;
document->protectedPage()->focusController().setFocusedElement(target.get(), document->protectedFrame().get(), focusOptions);
return;
}
target = target->parentOrShadowHostElement();
}
document->setFocusedElement(nullptr);
}
if (caretBrowsing)
document->protectedPage()->focusController().setFocusedElement(nullptr, document->protectedFrame().get());
}
void DragCaretController::paintDragCaret(LocalFrame* frame, GraphicsContext& p, const LayoutPoint& paintOffset) const
{
#if ENABLE(TEXT_CARET)
if (m_position.deepEquivalent().deprecatedNode() && m_position.deepEquivalent().deprecatedNode()->document().frame() == frame)
paintCaret(*m_position.deepEquivalent().deprecatedNode(), p, paintOffset, nullptr);
#else
UNUSED_PARAM(frame);
UNUSED_PARAM(p);
UNUSED_PARAM(paintOffset);
#endif
}
RefPtr<MutableStyleProperties> FrameSelection::copyTypingStyle() const
{
if (!m_typingStyle || !m_typingStyle->style())
return nullptr;
return m_typingStyle->style()->mutableCopy();
}
void FrameSelection::setTypingStyle(RefPtr<EditingStyle>&& style)
{
m_typingStyle = WTF::move(style);
}
void FrameSelection::clearTypingStyle()
{
m_typingStyle = nullptr;
}
bool FrameSelection::shouldDeleteSelection(const VisibleSelection& selection) const
{
#if PLATFORM(IOS_FAMILY)
if (m_document->frame() && m_document->frame()->selectionChangeCallbacksDisabled())
return true;
#endif
return m_document->editor().client()->shouldDeleteRange(selection.toNormalizedRange());
}
FloatRect FrameSelection::selectionBounds(ClipToVisibleContent clipToVisibleContent)
{
if (!m_document)
return LayoutRect();
updateSelectionAppearanceNow();
CheckedPtr renderView = m_document->renderView();
if (!renderView)
return LayoutRect();
if (!m_selection.range())
return LayoutRect();
#if PLATFORM(IOS_FAMILY)
auto selectionGeometries = RenderObject::collectSelectionGeometries(m_selection.range().value()).geometries;
IntRect visibleSelectionRect;
for (auto geometry : selectionGeometries)
visibleSelectionRect.unite(geometry.rect());
if (clipToVisibleContent == ClipToVisibleContent::No)
return visibleSelectionRect;
#else
auto& selection = renderView->selection();
auto visibleSelectionRect = selection.boundsClippedToVisibleContent();
if (clipToVisibleContent == ClipToVisibleContent::No)
return selection.bounds();
#endif
return intersection(visibleSelectionRect, renderView->frameView().visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect));
}
void FrameSelection::getClippedVisibleTextRectangles(Vector<FloatRect>& rectangles, TextRectangleHeight textRectHeight) const
{
if (!m_document->renderView())
return;
auto range = selection().toNormalizedRange();
if (!range)
return;
OptionSet<RenderObject::BoundingRectBehavior> behavior;
if (textRectHeight == TextRectangleHeight::SelectionHeight)
behavior.add(RenderObject::BoundingRectBehavior::UseSelectionHeight);
auto visibleContentRect = m_document->view()->visibleContentRect(ScrollableArea::LegacyIOSDocumentVisibleRect);
for (auto& rect : boundingBoxes(RenderObject::absoluteTextQuads(*range, behavior))) {
auto intersectionRect = intersection(rect, visibleContentRect);
if (!intersectionRect.isEmpty())
rectangles.append(intersectionRect);
}
}
// Scans logically forward from "start", including any child frames.
static RefPtr<HTMLFormElement> scanForForm(Element* start)
{
if (!start)
return nullptr;
for (Ref element : descendantsOfType<HTMLElement>(start->document())) {
if (RefPtr form = dynamicDowncast<HTMLFormElement>(element))
return form;
if (element->isFormListedElement())
return element->asFormListedElement()->form();
if (RefPtr frameElement = dynamicDowncast<HTMLFrameElementBase>(element)) {
if (RefPtr contentDocument = frameElement->contentDocument()) {
if (RefPtr frameResult = scanForForm(contentDocument->documentElement()))
return frameResult;
}
}
}
return nullptr;
}
static ValidatedFormListedElement* findFormControlElementAncestor(Element& element)
{
for (Ref ancestor : lineageOfType<Element>(element)) {
if (auto* formControlAncestor = ancestor->asValidatedFormListedElement())
return formControlAncestor;
}
return nullptr;
}
// We look for either the form containing the current focus, or for one immediately after it
RefPtr<HTMLFormElement> FrameSelection::currentForm() const
{
// Start looking either at the active (first responder) node, or where the selection is.
RefPtr start = m_document->focusedElement();
if (!start)
start = m_selection.start().anchorElementAncestor();
if (!start)
return nullptr;
if (RefPtr form = lineageOfType<HTMLFormElement>(*start).first())
return form;
if (RefPtr formControl = findFormControlElementAncestor(*start))
return formControl->form();
// Try walking forward in the node tree to find a form element.
return scanForForm(start.get());
}
void FrameSelection::revealSelection(const RevealSelectionOptions& revealSelectionOptions)
{
if (revealSelectionOptions.selectionRevealMode == SelectionRevealMode::DoNotReveal)
return;
if (isNone())
return;
updateSelectionAppearanceNow();
LayoutRect rect;
bool insideFixed = false;
if (isCaret())
rect = absoluteCaretBounds(&insideFixed);
else
rect = revealSelectionOptions.revealExtentOption == RevealExtentOption::RevealExtent ? VisiblePosition(m_selection.extent()).absoluteCaretBounds() : enclosingIntRect(selectionBounds(ClipToVisibleContent::No));
Position start = m_selection.start();
ASSERT(start.deprecatedNode());
if (!start.deprecatedNode() || !start.deprecatedNode()->renderer())
return;
#if PLATFORM(IOS_FAMILY)
if (m_scrollingSuppressCount)
return;
#endif
m_selectionRevealMode = SelectionRevealMode::DoNotReveal;
// FIXME: This code only handles scrolling the startContainer's layer, but
// the selection rect could intersect more than just that.
// See <rdar://problem/4799899>.
m_document->frame()->view()->setLastUserScrollType(LocalFrameView::UserScrollType::Implicit);
LocalFrameView::scrollRectToVisible(rect, *start.deprecatedNode()->renderer(), insideFixed, { revealSelectionOptions.selectionRevealMode, revealSelectionOptions.scrollAlignment, revealSelectionOptions.scrollAlignment, ShouldAllowCrossOriginScrolling::Yes, revealSelectionOptions.scrollBehavior, revealSelectionOptions.onlyAllowForwardScrolling });
updateAppearance();
#if PLATFORM(IOS_FAMILY)
if (m_document->page())
m_document->page()->chrome().client().notifyRevealedSelectionByScrollingFrame(*m_document->frame());
#endif
}
void FrameSelection::setSelectionFromNone()
{
// Put a caret inside the body if the entire frame is editable (either the
// entire WebView is editable or designMode is on for this document).
bool caretBrowsing = m_document->settings().caretBrowsingEnabled();
if (!m_document || !isNone() || !(m_document->hasEditableStyle() || caretBrowsing))
return;
if (RefPtr body = m_document->body())
setSelection(VisibleSelection(firstPositionInOrBeforeNode(body.get())));
}
bool FrameSelection::shouldChangeSelection(const VisibleSelection& newSelection) const
{
#if PLATFORM(IOS_FAMILY)
if (m_document->frame() && m_document->frame()->selectionChangeCallbacksDisabled())
return true;
#endif
return m_document->editor().shouldChangeSelection(selection(), newSelection, newSelection.affinity(), false);
}
bool FrameSelection::dispatchSelectStart()
{
RefPtr selectStartTarget = m_selection.extent().containerNode();
if (!selectStartTarget)
return true;
auto event = Event::create(eventNames().selectstartEvent, Event::CanBubble::Yes, Event::IsCancelable::Yes);
selectStartTarget->dispatchEvent(event);
return !event->defaultPrevented();
}
void FrameSelection::setShouldShowBlockCursor(bool shouldShowBlockCursor)
{
m_shouldShowBlockCursor = shouldShowBlockCursor;
protectedDocument()->updateLayoutIgnorePendingStylesheets();
updateAppearance();
}
void FrameSelection::updateAppearanceAfterUpdatingRendering()
{
if (auto* client = m_document->editor().client())
client->updateEditorStateAfterLayoutIfEditabilityChanged();
setCaretRectNeedsUpdate();
updateAndRevealSelection(m_selectionRevealIntent);
updateDataDetectorsForSelection();
}
#if ENABLE(TREE_DEBUGGING)
String FrameSelection::debugDescription() const
{
return m_selection.debugDescription();
}
void FrameSelection::showTreeForThis() const
{
m_selection.showTreeForThis();
}
#endif
std::optional<SimpleRange> FrameSelection::rangeByExtendingCurrentSelection(TextGranularity granularity) const
{
if (m_selection.isNone())
return std::nullopt;
auto frameSelection = makeUniqueRef<FrameSelection>();
frameSelection->setSelection(m_selection);
frameSelection->modify(Alteration::Move, SelectionDirection::Backward, granularity);
frameSelection->modify(Alteration::Extend, SelectionDirection::Forward, granularity);
return frameSelection->selection().toNormalizedRange();
}
#if PLATFORM(IOS_FAMILY)
void FrameSelection::expandSelectionToElementContainingCaretSelection()
{
auto range = elementRangeContainingCaretSelection();
if (!range)
return;
setSelection(VisibleSelection(*range));
}
std::optional<SimpleRange> FrameSelection::elementRangeContainingCaretSelection() const
{
auto element = deprecatedEnclosingBlockFlowElement(m_selection.visibleStart().deepEquivalent().deprecatedNode());
if (!element)
return std::nullopt;
auto start = VisiblePosition(makeContainerOffsetPosition(element, 0));
auto end = VisiblePosition(makeContainerOffsetPosition(element, element->countChildNodes()));
if (start.isNull() || end.isNull())
return std::nullopt;
auto selection = m_selection;
selection.setBase(start);
selection.setExtent(end);
return selection.toNormalizedRange();
}
void FrameSelection::expandSelectionToWordContainingCaretSelection()
{
VisibleSelection selection(wordSelectionContainingCaretSelection(m_selection));
if (selection.isCaretOrRange())
setSelection(selection);
}
std::optional<SimpleRange> FrameSelection::wordRangeContainingCaretSelection()
{
return wordSelectionContainingCaretSelection(m_selection).toNormalizedRange();
}
void FrameSelection::expandSelectionToStartOfWordContainingCaretSelection()
{
if (m_selection.isNone() || isStartOfDocument(m_selection.start()))
return;
VisiblePosition s1(m_selection.start());
VisiblePosition e1(m_selection.end());
VisibleSelection expanded(wordSelectionContainingCaretSelection(m_selection));
VisiblePosition s2(expanded.start());
// Don't allow the start to become greater after the expansion.
if (s2.isNull() || s2 > s1)
s2 = s1;
moveTo(s2, e1);
}
char16_t FrameSelection::characterInRelationToCaretSelection(int amount) const
{
auto position = m_selection.visibleStart();
if (amount < 0) {
int count = std::abs(amount);
for (int i = 0; i < count; i++)
position = position.previous();
return position.characterBefore();
}
for (int i = 0; i < amount; i++)
position = position.next();
return position.characterAfter();
}
bool FrameSelection::selectionAtWordStart() const
{
auto position = m_selection.visibleStart();
if (isStartOfParagraph(position))
return true;
unsigned previousCount = 0;
for (position = position.previous(); !position.isNull(); position = position.previous()) {
previousCount++;
if (isStartOfParagraph(position))
return previousCount != 1;
if (char16_t c = position.characterAfter())
return deprecatedIsSpaceOrNewline(c) || c == noBreakSpace || (u_ispunct(c) && c != ',' && c != '-' && c != '\'');
}
return true;
}
std::optional<SimpleRange> FrameSelection::rangeByMovingCurrentSelection(int amount) const
{
return rangeByAlteringCurrentSelection(Alteration::Move, amount);
}
std::optional<SimpleRange> FrameSelection::rangeByExtendingCurrentSelection(int amount) const
{
return rangeByAlteringCurrentSelection(Alteration::Extend, amount);
}
VisibleSelection FrameSelection::wordSelectionContainingCaretSelection(const VisibleSelection& selection)
{
if (selection.isNone())
return VisibleSelection();
ASSERT(selection.isCaretOrRange());
auto frameSelection = makeUniqueRef<FrameSelection>();
frameSelection->setSelection(selection);
Position startPosBeforeExpansion(selection.start());
Position endPosBeforeExpansion(selection.end());
VisiblePosition startVisiblePosBeforeExpansion(startPosBeforeExpansion);
VisiblePosition endVisiblePosBeforeExpansion(endPosBeforeExpansion);
if (endVisiblePosBeforeExpansion.isNull())
return VisibleSelection();
if (isEndOfParagraph(endVisiblePosBeforeExpansion)) {
char16_t c(endVisiblePosBeforeExpansion.characterBefore());
if (deprecatedIsSpaceOrNewline(c) || c == noBreakSpace) {
// End of paragraph with space.
return VisibleSelection();
}
}
// If at end of paragraph, move backwards one character.
// This has the effect of selecting the word on the line (which is
// what we want, rather than selecting past the end of the line).
if (isEndOfParagraph(endVisiblePosBeforeExpansion) && !isStartOfParagraph(endVisiblePosBeforeExpansion))
frameSelection->modify(FrameSelection::Alteration::Move, SelectionDirection::Backward, TextGranularity::CharacterGranularity);
VisibleSelection newSelection = frameSelection->selection();
newSelection.expandUsingGranularity(TextGranularity::WordGranularity);
frameSelection->setSelection(newSelection, defaultSetSelectionOptions(), AXTextStateChangeIntent(), CursorAlignOnScroll::IfNeeded, frameSelection->granularity());
Position startPos(frameSelection->selection().start());
Position endPos(frameSelection->selection().end());
// Expansion cannot be allowed to change selection so that it is no longer
// touches (or contains) the original, unexpanded selection.
// Enforce this on the way into these additional calculations to give them
// the best chance to yield a suitable answer.
if (startPos > startPosBeforeExpansion)
startPos = startPosBeforeExpansion;
if (endPos < endPosBeforeExpansion)
endPos = endPosBeforeExpansion;
VisiblePosition startVisiblePos(startPos);
VisiblePosition endVisiblePos(endPos);
if (startVisiblePos.isNull() || endVisiblePos.isNull()) {
// Start or end is nil
return VisibleSelection();
}
if (isEndOfLine(endVisiblePosBeforeExpansion)) {
VisiblePosition previous(endVisiblePos.previous());
if (previous == endVisiblePos) {
// Empty document
return VisibleSelection();
}
char16_t c(previous.characterAfter());
if (deprecatedIsSpaceOrNewline(c) || c == noBreakSpace) {
// Space at end of line
return VisibleSelection();
}
}
// Expansion has selected past end of line.
// Try repositioning backwards.
if (isEndOfLine(startVisiblePos) && isStartOfLine(endVisiblePos)) {
VisiblePosition previous(startVisiblePos.previous());
if (isEndOfLine(previous)) {
// On empty line
return VisibleSelection();
}
char16_t c(previous.characterAfter());
if (deprecatedIsSpaceOrNewline(c) || c == noBreakSpace) {
// Space at end of line
return VisibleSelection();
}
frameSelection->moveTo(startVisiblePos);
frameSelection->modify(FrameSelection::Alteration::Extend, SelectionDirection::Backward, TextGranularity::WordGranularity);
startPos = frameSelection->selection().start();
endPos = frameSelection->selection().end();
startVisiblePos = VisiblePosition(startPos);
endVisiblePos = VisiblePosition(endPos);
if (startVisiblePos.isNull() || endVisiblePos.isNull()) {
// Start or end is nil
return VisibleSelection();
}
}
// Now loop backwards until we find a non-space.
while (endVisiblePos != startVisiblePos) {
VisiblePosition previous(endVisiblePos.previous());
char16_t c(previous.characterAfter());
if (!deprecatedIsSpaceOrNewline(c) && c != noBreakSpace)
break;
endVisiblePos = previous;
}
// Expansion cannot be allowed to change selection so that it is no longer
// touches (or contains) the original, unexpanded selection.
// Enforce this on the way out of the function to preserve the invariant.
if (startVisiblePos > startVisiblePosBeforeExpansion)
startVisiblePos = startVisiblePosBeforeExpansion;
if (endVisiblePos < endVisiblePosBeforeExpansion)
endVisiblePos = endVisiblePosBeforeExpansion;
return VisibleSelection(startVisiblePos, endVisiblePos);
}
bool FrameSelection::selectionAtSentenceStart() const
{
auto position = m_selection.visibleStart();
if (position.isNull())
return false;
if (isStartOfParagraph(position))
return true;
bool sawSpace = false;
unsigned previousCount = 0;
for (position = position.previous(); !position.isNull(); position = position.previous()) {
previousCount++;
if (isStartOfParagraph(position))
return previousCount != 1 && (previousCount != 2 || !sawSpace);
if (auto c = position.characterAfter()) {
if (deprecatedIsSpaceOrNewline(c) || c == noBreakSpace)
sawSpace = true;
else
return c == '.' || c == '!' || c == '?';
}
}
return true;
}
std::optional<SimpleRange> FrameSelection::rangeByAlteringCurrentSelection(Alteration alteration, int amount) const
{
if (m_selection.isNone())
return std::nullopt;
if (!amount)
return m_selection.toNormalizedRange();
auto frameSelection = makeUniqueRef<FrameSelection>();
frameSelection->setSelection(m_selection);
SelectionDirection direction = amount > 0 ? SelectionDirection::Forward : SelectionDirection::Backward;
for (int i = 0; i < std::abs(amount); i++)
frameSelection->modify(alteration, direction, TextGranularity::CharacterGranularity);
return frameSelection->selection().toNormalizedRange();
}
void FrameSelection::clearCurrentSelection()
{
setSelection(VisibleSelection());
}
void FrameSelection::setCaretColor(const Color& caretColor)
{
if (m_caretColor != caretColor) {
m_caretColor = caretColor;
if (caretIsVisible() && isCaret())
invalidateCaretRect();
}
}
#endif // PLATFORM(IOS_FAMILY)
static bool containsEndpoints(const WeakPtr<Document, WeakPtrImplWithEventTargetData>& document, const std::optional<SimpleRange>& range)
{
return document && range && document->contains(range->start.container) && document->contains(range->end.container);
}
static bool containsEndpoints(const WeakPtr<Document, WeakPtrImplWithEventTargetData>& document, const Range& liveRange)
{
// Only need to check the start container because live ranges enforce the invariant that start and end have a common ancestor.
return document && document->contains(liveRange.startContainer());
}
bool FrameSelection::isInDocumentTree() const
{
return containsEndpoints(m_document, m_selection.range());
}
bool FrameSelection::isConnectedToDocument() const
{
return selection().document() == m_document.get();
}
RefPtr<Range> FrameSelection::associatedLiveRange()
{
if (!m_associatedLiveRange) {
if (auto range = m_selection.range(); containsEndpoints(m_document, range)) {
m_associatedLiveRange = createLiveRange(*range);
m_associatedLiveRange->didAssociateWithSelection();
}
}
return m_associatedLiveRange;
}
void FrameSelection::disassociateLiveRange()
{
if (auto previouslyAssociatedLiveRange = std::exchange(m_associatedLiveRange, nullptr))
previouslyAssociatedLiveRange->didDisassociateFromSelection();
}
void FrameSelection::associateLiveRange(Range& liveRange)
{
disassociateLiveRange();
m_associatedLiveRange = liveRange;
liveRange.didAssociateWithSelection();
updateFromAssociatedLiveRange();
}
void FrameSelection::updateFromAssociatedLiveRange()
{
ASSERT(m_associatedLiveRange);
if (!containsEndpoints(m_document, *m_associatedLiveRange))
disassociateLiveRange();
else {
// Don't use VisibleSelection's constructor that takes a SimpleRange, because it uses makeDeprecatedLegacyPosition instead of makeContainerOffsetPosition.
auto start = makeContainerOffsetPosition(m_associatedLiveRange->protectedStartContainer(), m_associatedLiveRange->startOffset());
auto end = makeContainerOffsetPosition(m_associatedLiveRange->protectedEndContainer(), m_associatedLiveRange->endOffset());
setSelection({ start, end }, defaultSetSelectionOptions() | SetSelectionOption::MaintainLiveRange);
}
}
void FrameSelection::updateOrDisassociateLiveRange(bool shouldMaintainLiveRange)
{
if (RefPtr associatedDocument = document()) {
if (associatedDocument->quirks().shouldReuseLiveRangeForSelectionUpdate()) {
auto range = m_selection.range();
if (!containsEndpoints(m_document, range)) {
// The selection was cleared or is now within a shadow tree.
disassociateLiveRange();
} else {
if (m_associatedLiveRange)
m_associatedLiveRange->updateFromSelection(*range);
}
return;
}
}
if (!shouldMaintainLiveRange)
disassociateLiveRange();
}
}
#if ENABLE(TREE_DEBUGGING)
void showTree(const WebCore::FrameSelection& selection)
{
selection.showTreeForThis();
}
void showTree(const WebCore::FrameSelection* selection)
{
if (selection)
selection->showTreeForThis();
}
#endif