/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "SelectionMovementUtils.h" #include "ErrorList.h" #include "WordMovementType.h" #include "mozilla/CaretAssociationHint.h" #include "mozilla/ContentIterator.h" #include "mozilla/Maybe.h" #include "mozilla/PresShell.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/Selection.h" #include "mozilla/dom/ShadowRoot.h" #include "mozilla/intl/BidiEmbeddingLevel.h" #include "nsBidiPresUtils.h" #include "nsBlockFrame.h" #include "nsCOMPtr.h" #include "nsCaret.h" #include "nsFrameSelection.h" #include "nsFrameTraversal.h" #include "nsIContent.h" #include "nsIFrame.h" #include "nsIFrameInlines.h" #include "nsLayoutUtils.h" #include "nsPresContext.h" #include "nsTextFrame.h" namespace mozilla { using namespace dom; template Result SelectionMovementUtils::MoveRangeBoundaryToSomewhere( const RangeBoundary& aRangeBoundary, nsDirection aDirection, CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel, nsSelectionAmount aAmount, PeekOffsetOptions aOptions, const dom::Element* aAncestorLimiter); template Result SelectionMovementUtils::MoveRangeBoundaryToSomewhere( const RawRangeBoundary& aRangeBoundary, nsDirection aDirection, CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel, nsSelectionAmount aAmount, PeekOffsetOptions aOptions, const dom::Element* aAncestorLimiter); template Result, nsresult> SelectionMovementUtils::MoveRangeBoundaryToSomewhere( const RangeBoundaryBase& aRangeBoundary, nsDirection aDirection, CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel, nsSelectionAmount aAmount, PeekOffsetOptions aOptions, const dom::Element* aAncestorLimiter) { MOZ_ASSERT(aDirection == eDirNext || aDirection == eDirPrevious); MOZ_ASSERT(aAmount == eSelectCharacter || aAmount == eSelectCluster || aAmount == eSelectWord || aAmount == eSelectBeginLine || aAmount == eSelectEndLine || aAmount == eSelectParagraph); if (!aRangeBoundary.IsSetAndValid()) { return Err(NS_ERROR_FAILURE); } if (!aRangeBoundary.GetContainer()->IsContent()) { return Err(NS_ERROR_FAILURE); } Result result = PeekOffsetForCaretMove( aRangeBoundary.GetContainer()->AsContent(), *aRangeBoundary.Offset( RangeBoundaryBase::OffsetFilter::kValidOrInvalidOffsets), aDirection, aHint, aCaretBidiLevel, aAmount, nsPoint{0, 0}, aOptions, aAncestorLimiter); if (result.isErr()) { return Err(NS_ERROR_FAILURE); } const PeekOffsetStruct& pos = result.unwrap(); if (NS_WARN_IF(!pos.mResultContent)) { return RangeBoundaryBase{}; } return RangeBoundaryBase{ pos.mResultContent, static_cast(pos.mContentOffset)}; } // FYI: This was done during a call of GetPrimaryFrameForCaretAtFocusNode. // Therefore, this may not be intended by the original author. // static Result SelectionMovementUtils::PeekOffsetForCaretMove( nsIContent* aContent, uint32_t aOffset, nsDirection aDirection, CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel, const nsSelectionAmount aAmount, const nsPoint& aDesiredCaretPos, PeekOffsetOptions aOptions, const Element* aAncestorLimiter) { const PrimaryFrameData frameForFocus = SelectionMovementUtils::GetPrimaryFrameForCaret( aContent, aOffset, aOptions.contains(PeekOffsetOption::Visual), aHint, aCaretBidiLevel); if (!frameForFocus) { return Err(NS_ERROR_FAILURE); } aOptions += {PeekOffsetOption::JumpLines, PeekOffsetOption::IsKeyboardSelect}; PeekOffsetStruct pos( aAmount, aDirection, static_cast(frameForFocus.mOffsetInFrameContent), aDesiredCaretPos, aOptions, eDefaultBehavior, aAncestorLimiter); nsresult rv = frameForFocus->PeekOffset(&pos); if (NS_FAILED(rv)) { return Err(rv); } return pos; } // static nsPrevNextBidiLevels SelectionMovementUtils::GetPrevNextBidiLevels( nsIContent* aNode, uint32_t aContentOffset, CaretAssociationHint aHint, bool aJumpLines, const Element* aAncestorLimiter) { // Get the level of the frames on each side nsDirection direction; nsPrevNextBidiLevels levels{}; levels.SetData(nullptr, nullptr, intl::BidiEmbeddingLevel::LTR(), intl::BidiEmbeddingLevel::LTR()); FrameAndOffset currentFrameAndOffset = SelectionMovementUtils::GetFrameForNodeOffset(aNode, aContentOffset, aHint); if (!currentFrameAndOffset) { return levels; } auto [frameStart, frameEnd] = currentFrameAndOffset->GetOffsets(); if (0 == frameStart && 0 == frameEnd) { direction = eDirPrevious; } else if (static_cast(frameStart) == currentFrameAndOffset.mOffsetInFrameContent) { direction = eDirPrevious; } else if (static_cast(frameEnd) == currentFrameAndOffset.mOffsetInFrameContent) { direction = eDirNext; } else { // we are neither at the beginning nor at the end of the frame, so we have // no worries intl::BidiEmbeddingLevel currentLevel = currentFrameAndOffset->GetEmbeddingLevel(); levels.SetData(currentFrameAndOffset.mFrame, currentFrameAndOffset.mFrame, currentLevel, currentLevel); return levels; } PeekOffsetOptions peekOffsetOptions{PeekOffsetOption::StopAtScroller}; if (aJumpLines) { peekOffsetOptions += PeekOffsetOption::JumpLines; } nsIFrame* newFrame = currentFrameAndOffset ->GetFrameFromDirection(direction, peekOffsetOptions, aAncestorLimiter) .mFrame; FrameBidiData currentBidi = currentFrameAndOffset->GetBidiData(); intl::BidiEmbeddingLevel currentLevel = currentBidi.embeddingLevel; intl::BidiEmbeddingLevel newLevel = newFrame ? newFrame->GetEmbeddingLevel() : currentBidi.baseLevel; // If not jumping lines, disregard br frames, since they might be positioned // incorrectly. // XXX This could be removed once bug 339786 is fixed. if (!aJumpLines) { if (currentFrameAndOffset->IsBrFrame()) { currentFrameAndOffset = {nullptr, 0u}; currentLevel = currentBidi.baseLevel; } if (newFrame && newFrame->IsBrFrame()) { newFrame = nullptr; newLevel = currentBidi.baseLevel; } } if (direction == eDirNext) { levels.SetData(currentFrameAndOffset.mFrame, newFrame, currentLevel, newLevel); } else { levels.SetData(newFrame, currentFrameAndOffset.mFrame, newLevel, currentLevel); } return levels; } // static Result SelectionMovementUtils::GetFrameFromLevel( nsIFrame* aFrameIn, nsDirection aDirection, intl::BidiEmbeddingLevel aBidiLevel) { if (!aFrameIn) { return Err(NS_ERROR_NULL_POINTER); } intl::BidiEmbeddingLevel foundLevel = intl::BidiEmbeddingLevel::LTR(); nsFrameIterator frameIterator(aFrameIn->PresContext(), aFrameIn, nsFrameIterator::Type::Leaf, false, // aVisual false, // aLockInScrollView false, // aFollowOOFs false // aSkipPopupChecks ); nsIFrame* foundFrame = aFrameIn; nsIFrame* theFrame = nullptr; do { theFrame = foundFrame; foundFrame = frameIterator.Traverse(aDirection == eDirNext); if (!foundFrame) { return Err(NS_ERROR_FAILURE); } foundLevel = foundFrame->GetEmbeddingLevel(); } while (foundLevel > aBidiLevel); MOZ_ASSERT(theFrame); return theFrame; } bool SelectionMovementUtils::AdjustFrameForLineStart(nsIFrame*& aFrame, uint32_t& aFrameOffset) { if (!aFrame->HasSignificantTerminalNewline()) { return false; } auto [start, end] = aFrame->GetOffsets(); if (aFrameOffset != static_cast(end)) { return false; } nsIFrame* nextSibling = aFrame->GetNextSibling(); if (!nextSibling) { return false; } aFrame = nextSibling; std::tie(start, end) = aFrame->GetOffsets(); aFrameOffset = start; return true; } static bool IsDisplayContents(const nsIContent* aContent) { return aContent->IsElement() && aContent->AsElement()->IsDisplayContents(); } // static FrameAndOffset SelectionMovementUtils::GetFrameForNodeOffset( const nsIContent* aNode, uint32_t aOffset, CaretAssociationHint aHint) { if (!aNode) { return {}; } if (static_cast(aOffset) < 0) { return {}; } if (!aNode->GetPrimaryFrame() && !IsDisplayContents(aNode)) { return {}; } nsIFrame *returnFrame = nullptr, *lastFrame = aNode->GetPrimaryFrame(); const nsIContent* theNode = nullptr; uint32_t offsetInFrameContent, offsetInLastFrameContent = aOffset; while (true) { if (returnFrame) { lastFrame = returnFrame; offsetInLastFrameContent = offsetInFrameContent; } offsetInFrameContent = aOffset; theNode = aNode; if (aNode->IsElement()) { uint32_t childIndex = 0; uint32_t numChildren = theNode->GetChildCount(); if (aHint == CaretAssociationHint::Before) { if (aOffset > 0) { childIndex = aOffset - 1; } else { childIndex = aOffset; } } else { MOZ_ASSERT(aHint == CaretAssociationHint::After); if (aOffset >= numChildren) { if (numChildren > 0) { childIndex = numChildren - 1; } else { childIndex = 0; } } else { childIndex = aOffset; } } if (childIndex > 0 || numChildren > 0) { nsCOMPtr childNode = theNode->GetChildAt_Deprecated(childIndex); if (!childNode) { break; } theNode = childNode; } // Now that we have the child node, check if it too // can contain children. If so, descend into child. if (theNode->IsElement() && theNode->GetChildCount() && !theNode->HasIndependentSelection()) { aNode = theNode; aOffset = aOffset > childIndex ? theNode->GetChildCount() : 0; continue; } // Check to see if theNode is a text node. If it is, translate // aOffset into an offset into the text node. if (const Text* textNode = Text::FromNode(theNode)) { if (theNode->GetPrimaryFrame()) { if (aOffset > childIndex) { uint32_t textLength = textNode->Length(); offsetInFrameContent = textLength; } else { offsetInFrameContent = 0; } } else { uint32_t numChildren = aNode->GetChildCount(); uint32_t newChildIndex = aHint == CaretAssociationHint::Before ? childIndex - 1 : childIndex + 1; if (newChildIndex < numChildren) { nsCOMPtr newChildNode = aNode->GetChildAt_Deprecated(newChildIndex); if (!newChildNode) { return {}; } aNode = newChildNode; aOffset = aHint == CaretAssociationHint::Before ? aNode->GetChildCount() : 0; continue; } // newChildIndex is illegal which means we're at first or last // child. Just use original node to get the frame. theNode = aNode; } } } // If the node is a ShadowRoot, the frame needs to be adjusted, // because a ShadowRoot does not get a frame. Its children are rendered // as children of the host. if (const ShadowRoot* shadow = ShadowRoot::FromNode(theNode)) { theNode = shadow->GetHost(); } returnFrame = theNode->GetPrimaryFrame(); if (returnFrame) { // FIXME: offsetInFrameContent has not been updated for theNode yet when // theNode is different from aNode. E.g., if a child at aNode and aOffset // is an , theNode is now the but offsetInFrameContent is the // offset for aNode. break; } if (aHint == CaretAssociationHint::Before) { if (aOffset > 0) { --aOffset; continue; } break; } if (aOffset < theNode->GetChildCount()) { ++aOffset; continue; } break; } // end while if (!returnFrame) { if (!lastFrame) { return {}; } returnFrame = lastFrame; offsetInFrameContent = offsetInLastFrameContent; } // If we ended up here and were asked to position the caret after a visible // break, let's return the frame on the next line instead if it exists. if (aOffset > 0 && (uint32_t)aOffset >= aNode->Length() && theNode == aNode->GetLastChild()) { nsIFrame* newFrame; nsLayoutUtils::IsInvisibleBreak(theNode, &newFrame); if (newFrame) { returnFrame = newFrame; offsetInFrameContent = 0; } } // find the child frame containing the offset we want int32_t unused = 0; returnFrame->GetChildFrameContainingOffset( static_cast(offsetInFrameContent), aHint == CaretAssociationHint::After, &unused, &returnFrame); return {returnFrame, offsetInFrameContent}; } // static RawRangeBoundary SelectionMovementUtils::GetFirstVisiblePointAtLeaf( const AbstractRange& aRange) { MOZ_ASSERT(aRange.IsPositioned()); MOZ_ASSERT_IF(aRange.IsStaticRange(), aRange.AsStaticRange()->IsValid()); // Currently, this is designed for non-collapsed range because this tries to // return a point in aRange. Therefore, if we need to return a nearest point // even outside aRange, we should add another utility method for making it // accept the outer range. MOZ_ASSERT(!aRange.Collapsed()); // The result should be a good point to put a UI to show something about the // start boundary of aRange. Therefore, we should find a content which is // visible or first unselectable one. // FIXME: ContentIterator does not support iterating content across shadow DOM // boundaries. We should improve it and here support it as an option. // If the start boundary is in a visible and selectable `Text`, let's return // the start boundary as-is. if (Text* const text = Text::FromNode(aRange.GetStartContainer())) { nsIFrame* const textFrame = text->GetPrimaryFrame(); if (textFrame && textFrame->IsSelectable()) { return aRange.StartRef().AsRaw(); } } // Iterate start of each node in the range so that the following loop checks // containers first, then, inner containers and leaf nodes. UnsafePreContentIterator iter; if (aRange.IsDynamicRange()) { if (NS_WARN_IF(NS_FAILED(iter.InitWithoutValidatingPoints( aRange.StartRef().AsRaw(), aRange.EndRef().AsRaw())))) { return {nullptr, nullptr}; } } else { if (NS_WARN_IF(NS_FAILED( iter.Init(aRange.StartRef().AsRaw(), aRange.EndRef().AsRaw())))) { return {nullptr, nullptr}; } } // We need to ignore unselectable nodes if the range started from an // unselectable node, for example, if starting from the document start but // only in which is shown as a modal one is selectable, we want to // treat the visible selection starts from the start of the first visible // thing in the . // Additionally, let's stop when we find first unselectable element in a // selectable node. Then, the caller can show something at the end edge of // the unselectable element rather than the leaf to make it clear that the // selection range starts before the unselectable element. bool foundSelectableContainer = [&]() { nsIContent* const startContainer = nsIContent::FromNode(aRange.GetStartContainer()); return startContainer && startContainer->IsSelectable(); }(); for (iter.First(); !iter.IsDone(); iter.Next()) { nsIContent* const content = nsIContent::FromNodeOrNull(iter.GetCurrentNode()); if (MOZ_UNLIKELY(!content)) { break; } nsIFrame* const primaryFrame = content->GetPrimaryFrame(); // If the content does not have any layout information, let's continue. if (!primaryFrame) { continue; } // FYI: We don't need to skip invisible
at scanning start of visible // thing like what we're doing in GetVisibleRangeEnd() because if we reached // it, the selection range starts from end of the line so that putting UI // around it is reasonable. // If the frame is unselectable, we need to stop scanning now if we're // scanning in a selectable range. if (!primaryFrame->IsSelectable()) { // If we have not found a selectable content yet (this is the case when // only a part of the document is selectable like the case // explained above), we should just ignore the unselectable content until // we find first selectable element. Then, the caller can show something // before the first child of the first selectable container in the range. if (!foundSelectableContainer) { continue; } // If we have already found a selectable content and now we reached an // unselectable element, we should return the point of the unselectable // element. Then, the caller can show something at the start edge of the // unselectable element to show users that the range contains the // unselectable element. return {content->GetParentNode(), content->GetPreviousSibling()}; } // We found a visible (and maybe selectable) Text, return the start of it. if (content->IsText()) { return {content, 0u}; } // We found a replaced element such as
, , form widget return the // point at the content. if (primaryFrame->IsReplaced()) { return {content->GetParentNode(), content->GetPreviousSibling()}; } //