diff --git a/accessible/generic/HyperTextAccessible.cpp b/accessible/generic/HyperTextAccessible.cpp index 7ebeee2dddeb..9618c20ad388 100644 --- a/accessible/generic/HyperTextAccessible.cpp +++ b/accessible/generic/HyperTextAccessible.cpp @@ -35,6 +35,7 @@ #include "mozilla/HTMLEditor.h" #include "mozilla/IntegerRange.h" #include "mozilla/PresShell.h" +#include "mozilla/SelectionMovementUtils.h" #include "mozilla/dom/Element.h" #include "mozilla/dom/HTMLBRElement.h" #include "mozilla/dom/Selection.h" @@ -658,11 +659,10 @@ int32_t HyperTextAccessible::CaretLineNumber() { nsIContent* caretContent = caretNode->AsContent(); if (!nsCoreUtils::IsAncestorOf(GetNode(), caretContent)) return -1; - int32_t returnOffsetUnused; uint32_t caretOffset = domSel->FocusOffset(); CaretAssociationHint hint = frameSelection->GetHint(); - nsIFrame* caretFrame = frameSelection->GetFrameForNodeOffset( - caretContent, caretOffset, hint, &returnOffsetUnused); + nsIFrame* caretFrame = SelectionMovementUtils::GetFrameForNodeOffset( + caretContent, caretOffset, hint); NS_ENSURE_TRUE(caretFrame, -1); AutoAssertNoDomMutations guard; // The nsILineIterators below will break if diff --git a/dom/base/Selection.cpp b/dom/base/Selection.cpp index e4d0374c949d..e1cf6a20ac16 100644 --- a/dom/base/Selection.cpp +++ b/dom/base/Selection.cpp @@ -34,6 +34,7 @@ #include "mozilla/PresShell.h" #include "mozilla/RangeBoundary.h" #include "mozilla/RangeUtils.h" +#include "mozilla/SelectionMovementUtils.h" #include "mozilla/StackWalk.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/Telemetry.h" @@ -1536,16 +1537,15 @@ nsresult Selection::StyledRanges::GetIndicesForInterval( nsIFrame* Selection::GetPrimaryFrameForAnchorNode() const { MOZ_ASSERT(mSelectionType == SelectionType::eNormal); - int32_t frameOffset = 0; nsCOMPtr content = do_QueryInterface(GetAnchorNode()); if (content && mFrameSelection) { - return nsFrameSelection::GetFrameForNodeOffset( - content, AnchorOffset(), mFrameSelection->GetHint(), &frameOffset); + return SelectionMovementUtils::GetFrameForNodeOffset( + content, AnchorOffset(), mFrameSelection->GetHint()); } return nullptr; } -Selection::PrimaryFrameData Selection::GetPrimaryFrameForCaretAtFocusNode( +PrimaryFrameData Selection::GetPrimaryFrameForCaretAtFocusNode( bool aVisual) const { nsIContent* content = nsIContent::FromNodeOrNull(GetFocusNode()); if (!content || !mFrameSelection || !mFrameSelection->GetPresShell()) { @@ -1558,61 +1558,8 @@ Selection::PrimaryFrameData Selection::GetPrimaryFrameForCaretAtFocusNode( CaretAssociationHint hint = mFrameSelection->GetHint(); intl::BidiEmbeddingLevel caretBidiLevel = mFrameSelection->GetCaretBidiLevel(); - return Selection::GetPrimaryFrameForCaret(content, FocusOffset(), aVisual, - hint, caretBidiLevel); -} - -// static -Selection::PrimaryFrameData Selection::GetPrimaryFrameForCaret( - nsIContent* aContent, uint32_t aOffset, bool aVisual, - CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel) { - MOZ_ASSERT(aContent); - - { - const PrimaryFrameData result = GetPrimaryOrCaretFrameForNodeOffset( - aContent, aOffset, aVisual, aHint, aCaretBidiLevel); - if (result.mFrame) { - return result; - } - } - - // If aContent is whitespace only, we promote focus node to parent because - // whitespace only node might have no frame. - - if (!aContent->TextIsOnlyWhitespace()) { - return {}; - } - - nsIContent* parent = aContent->GetParent(); - if (NS_WARN_IF(!parent)) { - return {}; - } - const Maybe offset = parent->ComputeIndexOf(aContent); - if (NS_WARN_IF(offset.isNothing())) { - return {}; - } - return GetPrimaryOrCaretFrameForNodeOffset(parent, *offset, aVisual, aHint, - aCaretBidiLevel); -} - -// static -Selection::PrimaryFrameData Selection::GetPrimaryOrCaretFrameForNodeOffset( - nsIContent* aContent, uint32_t aOffset, bool aVisual, - CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel) { - if (aVisual) { - const nsCaret::CaretFrameData result = nsCaret::GetCaretFrameForNodeOffset( - nullptr, aContent, static_cast(aOffset), aHint, - aCaretBidiLevel, - aContent && aContent->IsEditable() ? nsCaret::ForceEditableRegion::Yes - : nsCaret::ForceEditableRegion::No); - return {result.mFrame, static_cast(result.mOffsetInFrameContent), - result.mHint}; - } - - int32_t offset = 0; - nsIFrame* theFrame = nsFrameSelection::GetFrameForNodeOffset( - aContent, static_cast(aOffset), aHint, &offset); - return {theFrame, static_cast(offset), aHint}; + return SelectionMovementUtils::GetPrimaryFrameForCaret( + content, FocusOffset(), aVisual, hint, caretBidiLevel); } void Selection::SelectFramesOf(nsIContent* aContent, bool aSelected) const { @@ -3235,12 +3182,12 @@ nsIFrame* Selection::GetSelectionEndPointGeometry(SelectionRegion aRegion, nsCOMPtr content = do_QueryInterface(node); NS_ENSURE_TRUE(content.get(), nullptr); - int32_t frameOffset = 0; - frame = nsFrameSelection::GetFrameForNodeOffset( + uint32_t frameOffset = 0; + frame = SelectionMovementUtils::GetFrameForNodeOffset( content, nodeOffset, mFrameSelection->GetHint(), &frameOffset); if (!frame) return nullptr; - nsFrameSelection::AdjustFrameForLineStart(frame, frameOffset); + SelectionMovementUtils::AdjustFrameForLineStart(frame, frameOffset); // Figure out what node type we have, then get the // appropriate rect for its nodeOffset. @@ -3249,7 +3196,7 @@ nsIFrame* Selection::GetSelectionEndPointGeometry(SelectionRegion aRegion, nsPoint pt(0, 0); if (isText) { nsIFrame* childFrame = nullptr; - frameOffset = 0; + int32_t frameOffset = 0; nsresult rv = frame->GetChildFrameContainingOffset( nodeOffset, mFrameSelection->GetHint() == CaretAssociationHint::After, &frameOffset, &childFrame); diff --git a/dom/base/Selection.h b/dom/base/Selection.h index cc2faff778c5..4d2fc160dd07 100644 --- a/dom/base/Selection.h +++ b/dom/base/Selection.h @@ -16,8 +16,6 @@ #include "mozilla/WeakPtr.h" #include "mozilla/dom/Highlight.h" #include "mozilla/dom/StyledRange.h" -#include "mozilla/intl/Bidi.h" -#include "mozilla/intl/BidiEmbeddingLevel.h" #include "nsDirection.h" #include "nsISelectionController.h" #include "nsISelectionListener.h" @@ -48,6 +46,7 @@ class PostContentIterator; enum class CaretAssociationHint; enum class TableSelectionMode : uint32_t; struct AutoPrepareFocusRange; +struct PrimaryFrameData; namespace dom { class DocGroup; } // namespace dom @@ -258,25 +257,6 @@ class Selection final : public nsSupportsWeakReference, nsIFrame* GetPrimaryFrameForAnchorNode() const; - struct MOZ_STACK_CLASS PrimaryFrameData final { - // The frame which should be used to layout the caret. - nsIFrame* mFrame = nullptr; - // The offset in content of mFrame. This is valid only when mFrame is not - // nullptr. - uint32_t mOffsetInFrameContent = 0; - // Whether the caret should be put before or after the point. This is valid - // only when mFrame is not nullptr. - CaretAssociationHint mHint{0}; // Before - }; - - /** - * Get primary frame and some other data for putting caret or extending - * selection at the point. - */ - static PrimaryFrameData GetPrimaryFrameForCaret( - nsIContent* aContent, uint32_t aOffset, bool aVisual, - CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel); - /** * Get primary frame and some other data for putting caret or extending * selection at the focus point. @@ -751,13 +731,6 @@ class Selection final : public nsSupportsWeakReference, Document* aDocument, ErrorResult&); - // This is helper method for GetPrimaryFrameForCaret. - // If aVisual is true, this returns caret frame. - // If false, this returns primary frame. - static PrimaryFrameData GetPrimaryOrCaretFrameForNodeOffset( - nsIContent* aContent, uint32_t aOffset, bool aVisual, - CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel); - // Get the cached value for nsTextFrame::GetPointFromOffset. nsresult GetCachedFrameOffset(nsIFrame* aFrame, int32_t inOffset, nsPoint& aPoint); diff --git a/dom/events/ContentEventHandler.cpp b/dom/events/ContentEventHandler.cpp index f497348e25d5..a4513040def9 100644 --- a/dom/events/ContentEventHandler.cpp +++ b/dom/events/ContentEventHandler.cpp @@ -15,6 +15,7 @@ #include "mozilla/PresShell.h" #include "mozilla/RangeBoundary.h" #include "mozilla/RangeUtils.h" +#include "mozilla/SelectionMovementUtils.h" #include "mozilla/TextComposition.h" #include "mozilla/TextEditor.h" #include "mozilla/TextEvents.h" @@ -1119,11 +1120,10 @@ nsresult ContentEventHandler::ExpandToClusterBoundary( NS_ASSERTION(*aXPOffset <= aTextNode.TextLength(), "offset is out of range."); MOZ_DIAGNOSTIC_ASSERT(mDocument->GetPresShell()); - int32_t offsetInFrame; CaretAssociationHint hint = aForward ? CaretAssociationHint::Before : CaretAssociationHint::After; - nsIFrame* frame = nsFrameSelection::GetFrameForNodeOffset( - &aTextNode, int32_t(*aXPOffset), hint, &offsetInFrame); + nsIFrame* frame = SelectionMovementUtils::GetFrameForNodeOffset( + &aTextNode, int32_t(*aXPOffset), hint); if (frame) { auto [startOffset, endOffset] = frame->GetOffsets(); if (*aXPOffset == static_cast(startOffset) || diff --git a/layout/base/AccessibleCaretManager.cpp b/layout/base/AccessibleCaretManager.cpp index 725bbc7b7d7a..5538dc3dd417 100644 --- a/layout/base/AccessibleCaretManager.cpp +++ b/layout/base/AccessibleCaretManager.cpp @@ -22,6 +22,7 @@ #include "mozilla/IMEStateManager.h" #include "mozilla/IntegerPrintfMacros.h" #include "mozilla/PresShell.h" +#include "mozilla/SelectionMovementUtils.h" #include "mozilla/StaticAnalysisFunctions.h" #include "mozilla/StaticPrefs_layout.h" #include "nsCaret.h" @@ -656,9 +657,8 @@ nsresult AccessibleCaretManager::SelectWordOrShortcut(const nsPoint& aPoint) { if (offsets.content) { RefPtr frameSelection = GetFrameSelection(); if (frameSelection) { - int32_t offset; - nsIFrame* theFrame = nsFrameSelection::GetFrameForNodeOffset( - offsets.content, offsets.offset, offsets.associate, &offset); + nsIFrame* theFrame = SelectionMovementUtils::GetFrameForNodeOffset( + offsets.content, offsets.offset, offsets.associate); if (theFrame && theFrame != ptFrame) { SetSelectionDragState(true); frameSelection->HandleClick( @@ -1077,9 +1077,11 @@ nsIFrame* AccessibleCaretManager::GetFrameForFirstRangeStartOrLastRangeEnd( hint = CaretAssociationHint::Before; } - nsCOMPtr startContent = do_QueryInterface(startNode); - nsIFrame* startFrame = nsFrameSelection::GetFrameForNodeOffset( - startContent, nodeOffset, hint, aOutOffset); + nsCOMPtr startContent = nsIContent::FromNodeOrNull(startNode); + uint32_t outOffset = 0; + nsIFrame* startFrame = SelectionMovementUtils::GetFrameForNodeOffset( + startContent, nodeOffset, hint, &outOffset); + *aOutOffset = static_cast(outOffset); if (!startFrame) { ErrorResult err; diff --git a/layout/base/nsCaret.cpp b/layout/base/nsCaret.cpp index 3537e2911977..d66d55d6bb2b 100644 --- a/layout/base/nsCaret.cpp +++ b/layout/base/nsCaret.cpp @@ -37,6 +37,7 @@ #include "mozilla/dom/Selection.h" #include "nsIBidiKeyboard.h" #include "nsContentUtils.h" +#include "SelectionMovementUtils.h" using namespace mozilla; using namespace mozilla::dom; @@ -49,76 +50,6 @@ using BidiEmbeddingLevel = mozilla::intl::BidiEmbeddingLevel; // like an insignificant dot static const int32_t kMinBidiIndicatorPixels = 2; -/** - * Find the first frame in an in-order traversal of the frame subtree rooted - * at aFrame which is either a text frame logically at the end of a line, - * or which is aStopAtFrame. Return null if no such frame is found. We don't - * descend into the children of non-eLineParticipant frames. - */ -static nsIFrame* CheckForTrailingTextFrameRecursive(nsIFrame* aFrame, - nsIFrame* aStopAtFrame) { - if (aFrame == aStopAtFrame || - ((aFrame->IsTextFrame() && - (static_cast(aFrame))->IsAtEndOfLine()))) { - return aFrame; - } - if (!aFrame->IsLineParticipant()) { - return nullptr; - } - - for (nsIFrame* f : aFrame->PrincipalChildList()) { - if (nsIFrame* r = CheckForTrailingTextFrameRecursive(f, aStopAtFrame)) { - return r; - } - } - return nullptr; -} - -static nsLineBox* FindContainingLine(nsIFrame* aFrame) { - while (aFrame && aFrame->IsLineParticipant()) { - nsIFrame* parent = aFrame->GetParent(); - nsBlockFrame* blockParent = do_QueryFrame(parent); - if (blockParent) { - bool isValid; - nsBlockInFlowLineIterator iter(blockParent, aFrame, &isValid); - return isValid ? iter.GetLine().get() : nullptr; - } - aFrame = parent; - } - return nullptr; -} - -static void AdjustCaretFrameForLineEnd(nsIFrame** aFrame, int32_t* aOffset, - bool aEditableOnly) { - nsLineBox* line = FindContainingLine(*aFrame); - if (!line) { - return; - } - int32_t count = line->GetChildCount(); - for (nsIFrame* f = line->mFirstChild; count > 0; - --count, f = f->GetNextSibling()) { - nsIFrame* r = CheckForTrailingTextFrameRecursive(f, *aFrame); - if (r == *aFrame) { - return; - } - if (!r) { - continue; - } - // If found text frame is non-editable but the start frame content is - // editable, we don't want to put caret into the non-editable text node. - // We should return the given frame as-is in this case. - if (aEditableOnly && !r->GetContent()->IsEditable()) { - return; - } - // We found our frame, but we may not be able to properly paint the caret - // if -moz-user-modify differs from our actual frame. - MOZ_ASSERT(r->IsTextFrame(), "Expected text frame"); - *aFrame = r; - *aOffset = (static_cast(r))->GetContentEnd(); - return; - } -} - nsCaret::nsCaret() : mOverrideOffset(0), mBlinkCount(-1), @@ -431,9 +362,10 @@ nsIFrame* nsCaret::GetFrameAndOffset(const Selection* aSelection, nsIContent* contentNode = focusNode->AsContent(); nsFrameSelection* frameSelection = aSelection->GetFrameSelection(); BidiEmbeddingLevel bidiLevel = frameSelection->GetCaretBidiLevel(); - const nsCaret::CaretFrameData result = nsCaret::GetCaretFrameForNodeOffset( - frameSelection, contentNode, focusOffset, frameSelection->GetHint(), - bidiLevel, ForceEditableRegion::No); + const CaretFrameData result = + SelectionMovementUtils::GetCaretFrameForNodeOffset( + frameSelection, contentNode, focusOffset, frameSelection->GetHint(), + bidiLevel, ForceEditableRegion::No); // FIXME: It's odd to update nsFrameSelection within this method which is // named as a getter. if (result.mFrame) { @@ -702,210 +634,6 @@ void nsCaret::StopBlinking() { } } -nsCaret::CaretFrameData nsCaret::GetCaretFrameForNodeOffset( - const nsFrameSelection* aFrameSelection, nsIContent* aContentNode, - int32_t aOffset, CaretAssociationHint aFrameHint, - BidiEmbeddingLevel aBidiLevel, ForceEditableRegion aForceEditableRegion) { - if (!aContentNode || !aContentNode->IsInComposedDoc()) { - return {}; - } - - CaretFrameData result; - result.mHint = aFrameHint; - if (aFrameSelection) { - PresShell* presShell = aFrameSelection->GetPresShell(); - if (!presShell) { - return {}; - } - - if (presShell->GetDocument() != aContentNode->GetComposedDoc()) { - return {}; - } - - result.mHint = aFrameSelection->GetHint(); - } - - MOZ_ASSERT_IF(aForceEditableRegion == ForceEditableRegion::Yes, - aContentNode->IsEditable()); - - result.mFrame = result.mUnadjustedFrame = - nsFrameSelection::GetFrameForNodeOffset(aContentNode, aOffset, aFrameHint, - &result.mOffsetInFrameContent); - if (!result.mFrame) { - return {}; - } - - if (nsFrameSelection::AdjustFrameForLineStart(result.mFrame, - result.mOffsetInFrameContent)) { - result.mHint = CaretAssociationHint::After; - } else { - // if the frame is after a text frame that's logically at the end of the - // line (e.g. if the frame is a
frame), then put the caret at the end - // of that text frame instead. This way, the caret will be positioned as if - // trailing whitespace was not trimmed. - AdjustCaretFrameForLineEnd( - &result.mFrame, &result.mOffsetInFrameContent, - aForceEditableRegion == ForceEditableRegion::Yes); - } - - // Mamdouh : modification of the caret to work at rtl and ltr with Bidi - // - // Direction Style from visibility->mDirection - // ------------------ - if (result.mFrame->PresContext()->BidiEnabled()) { - // If there has been a reflow, take the caret Bidi level to be the level of - // the current frame - if (aBidiLevel & BIDI_LEVEL_UNDEFINED) { - aBidiLevel = result.mFrame->GetEmbeddingLevel(); - } - - nsIFrame* frameBefore; - nsIFrame* frameAfter; - BidiEmbeddingLevel - levelBefore; // Bidi level of the character before the caret - BidiEmbeddingLevel - levelAfter; // Bidi level of the character after the caret - - auto [start, end] = result.mFrame->GetOffsets(); - if (start == 0 || end == 0 || start == result.mOffsetInFrameContent || - end == result.mOffsetInFrameContent) { - nsPrevNextBidiLevels levels = nsFrameSelection::GetPrevNextBidiLevels( - aContentNode, aOffset, result.mHint, false); - - /* Boundary condition, we need to know the Bidi levels of the characters - * before and after the caret */ - if (levels.mFrameBefore || levels.mFrameAfter) { - frameBefore = levels.mFrameBefore; - frameAfter = levels.mFrameAfter; - levelBefore = levels.mLevelBefore; - levelAfter = levels.mLevelAfter; - - if ((levelBefore != levelAfter) || (aBidiLevel != levelBefore)) { - aBidiLevel = std::max(aBidiLevel, - std::min(levelBefore, levelAfter)); // rule c3 - aBidiLevel = std::min(aBidiLevel, - std::max(levelBefore, levelAfter)); // rule c4 - if (aBidiLevel == levelBefore || // rule c1 - (aBidiLevel > levelBefore && aBidiLevel < levelAfter && - aBidiLevel.IsSameDirection(levelBefore)) || // rule c5 - (aBidiLevel < levelBefore && aBidiLevel > levelAfter && - aBidiLevel.IsSameDirection(levelBefore))) // rule c9 - { - if (result.mFrame != frameBefore) { - if (frameBefore) { // if there is a frameBefore, move into it - result.mFrame = frameBefore; - std::tie(start, end) = result.mFrame->GetOffsets(); - result.mOffsetInFrameContent = end; - } else { - // if there is no frameBefore, we must be at the beginning of - // the line so we stay with the current frame. Exception: when - // the first frame on the line has a different Bidi level from - // the paragraph level, there is no real frame for the caret to - // be in. We have to find the visually first frame on the line. - BidiEmbeddingLevel baseLevel = frameAfter->GetBaseLevel(); - if (baseLevel != levelAfter) { - PeekOffsetStruct pos(eSelectBeginLine, eDirPrevious, 0, - nsPoint(0, 0), - {PeekOffsetOption::StopAtScroller, - PeekOffsetOption::Visual}); - if (NS_SUCCEEDED(frameAfter->PeekOffset(&pos))) { - result.mFrame = pos.mResultFrame; - result.mOffsetInFrameContent = pos.mContentOffset; - } - } - } - } - } else if (aBidiLevel == levelAfter || // rule c2 - (aBidiLevel > levelBefore && aBidiLevel < levelAfter && - aBidiLevel.IsSameDirection(levelAfter)) || // rule c6 - (aBidiLevel < levelBefore && aBidiLevel > levelAfter && - aBidiLevel.IsSameDirection(levelAfter))) // rule c10 - { - if (result.mFrame != frameAfter) { - if (frameAfter) { - // if there is a frameAfter, move into it - result.mFrame = frameAfter; - std::tie(start, end) = result.mFrame->GetOffsets(); - result.mOffsetInFrameContent = start; - } else { - // if there is no frameAfter, we must be at the end of the line - // so we stay with the current frame. - // Exception: when the last frame on the line has a different - // Bidi level from the paragraph level, there is no real frame - // for the caret to be in. We have to find the visually last - // frame on the line. - BidiEmbeddingLevel baseLevel = frameBefore->GetBaseLevel(); - if (baseLevel != levelBefore) { - PeekOffsetStruct pos(eSelectEndLine, eDirNext, 0, - nsPoint(0, 0), - {PeekOffsetOption::StopAtScroller, - PeekOffsetOption::Visual}); - if (NS_SUCCEEDED(frameBefore->PeekOffset(&pos))) { - result.mFrame = pos.mResultFrame; - result.mOffsetInFrameContent = pos.mContentOffset; - } - } - } - } - } else if (aBidiLevel > levelBefore && - aBidiLevel < levelAfter && // rule c7/8 - // before and after have the same parity - levelBefore.IsSameDirection(levelAfter) && - // caret has different parity - !aBidiLevel.IsSameDirection(levelAfter)) { - MOZ_ASSERT_IF(aFrameSelection && aFrameSelection->GetPresShell(), - aFrameSelection->GetPresShell()->GetPresContext() == - frameAfter->PresContext()); - Result frameOrError = - nsFrameSelection::GetFrameFromLevel(frameAfter, eDirNext, - aBidiLevel); - if (MOZ_LIKELY(frameOrError.isOk())) { - result.mFrame = frameOrError.unwrap(); - std::tie(start, end) = result.mFrame->GetOffsets(); - levelAfter = result.mFrame->GetEmbeddingLevel(); - if (aBidiLevel.IsRTL()) { - // c8: caret to the right of the rightmost character - result.mOffsetInFrameContent = levelAfter.IsRTL() ? start : end; - } else { - // c7: caret to the left of the leftmost character - result.mOffsetInFrameContent = levelAfter.IsRTL() ? end : start; - } - } - } else if (aBidiLevel < levelBefore && - aBidiLevel > levelAfter && // rule c11/12 - // before and after have the same parity - levelBefore.IsSameDirection(levelAfter) && - // caret has different parity - !aBidiLevel.IsSameDirection(levelAfter)) { - MOZ_ASSERT_IF(aFrameSelection && aFrameSelection->GetPresShell(), - aFrameSelection->GetPresShell()->GetPresContext() == - frameBefore->PresContext()); - Result frameOrError = - nsFrameSelection::GetFrameFromLevel(frameBefore, eDirPrevious, - aBidiLevel); - if (MOZ_LIKELY(frameOrError.isOk())) { - result.mFrame = frameOrError.unwrap(); - std::tie(start, end) = result.mFrame->GetOffsets(); - levelBefore = result.mFrame->GetEmbeddingLevel(); - if (aBidiLevel.IsRTL()) { - // c12: caret to the left of the leftmost character - result.mOffsetInFrameContent = - levelBefore.IsRTL() ? end : start; - } else { - // c11: caret to the right of the rightmost character - result.mOffsetInFrameContent = - levelBefore.IsRTL() ? start : end; - } - } - } - } - } - } - } - - return result; -} - size_t nsCaret::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const { size_t total = aMallocSizeOf(this); if (mPresShell) { diff --git a/layout/base/nsCaret.h b/layout/base/nsCaret.h index b49caaea9639..43329c827ffb 100644 --- a/layout/base/nsCaret.h +++ b/layout/base/nsCaret.h @@ -9,7 +9,6 @@ #ifndef nsCaret_h__ #define nsCaret_h__ -#include "mozilla/intl/BidiEmbeddingLevel.h" #include "mozilla/MemoryReporting.h" #include "mozilla/dom/Selection.h" #include "nsCoord.h" @@ -179,42 +178,6 @@ class nsCaret final : public nsISelectionListener { static nsIFrame* GetGeometry(const mozilla::dom::Selection* aSelection, nsRect* aRect); - enum class ForceEditableRegion { No, Yes }; - - struct MOZ_STACK_CLASS CaretFrameData final { - // The frame which should be used to compute caret geometry when caret is - // put at a specific DOM point. - nsIFrame* mFrame = nullptr; - // The frame which is found only from a DOM point. This frame becomes - // different from mFrame when the point is around end of a line or - // at a bidi text boundary. - nsIFrame* mUnadjustedFrame = nullptr; - // The content offset when caret is in the content of mFrame. This is - // valid only when mFrame is not nullptr. - int32_t mOffsetInFrameContent = 0; - // Whether the caret should be put before or after the point. - CaretAssociationHint mHint{0}; // Before - }; - - /** - * Return a frame for considering caret geometry. - * - * @param aFrameSelection [optional] If this is specified and selection in - * aContent is not managed by the specified - * instance, return nullptr. - * @param aContentNode The content node where selection is collapsed. - * @param aOffset Collapsed position in aContentNode - * @param aFrameHint Caret association hint. - * @param aBidiLevel - * @param aForceEditableRegion Whether selection should be limited in - * editable region or not. - */ - static CaretFrameData GetCaretFrameForNodeOffset( - const nsFrameSelection* aFrameSelection, nsIContent* aContentNode, - int32_t aOffset, CaretAssociationHint aFrameHint, - mozilla::intl::BidiEmbeddingLevel aBidiLevel, - ForceEditableRegion aForceEditableRegion); - static nsRect GetGeometryForFrame(nsIFrame* aFrame, int32_t aFrameOffset, nscoord* aBidiIndicatorSize); diff --git a/layout/generic/SelectionMovementUtils.cpp b/layout/generic/SelectionMovementUtils.cpp new file mode 100644 index 000000000000..a71da2c20586 --- /dev/null +++ b/layout/generic/SelectionMovementUtils.cpp @@ -0,0 +1,682 @@ +/* -*- 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 "ErrorList.h" +#include "SelectionMovementUtils.h" +#include "mozilla/CaretAssociationHint.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 "nsCaret.h" +#include "nsCOMPtr.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; +// 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 PrimaryFrameData frameForFocus = + SelectionMovementUtils::GetPrimaryFrameForCaret( + aContent, aOffset, aOptions.contains(PeekOffsetOption::Visual), aHint, + aCaretBidiLevel); + if (!frameForFocus.mFrame) { + return Err(NS_ERROR_FAILURE); + } + + aOptions += {PeekOffsetOption::JumpLines, PeekOffsetOption::IsKeyboardSelect}; + PeekOffsetStruct pos( + aAmount, aDirection, + static_cast(frameForFocus.mOffsetInFrameContent), + aDesiredCaretPos, aOptions); + nsresult rv = frameForFocus.mFrame->PeekOffset(&pos); + if (NS_FAILED(rv)) { + return Err(rv); + } + return pos; +} + +// static +nsPrevNextBidiLevels SelectionMovementUtils::GetPrevNextBidiLevels( + nsIContent* aNode, uint32_t aContentOffset, CaretAssociationHint aHint, + bool aJumpLines) { + // Get the level of the frames on each side + nsIFrame* currentFrame; + uint32_t currentOffset; + nsDirection direction; + + nsPrevNextBidiLevels levels{}; + levels.SetData(nullptr, nullptr, intl::BidiEmbeddingLevel::LTR(), + intl::BidiEmbeddingLevel::LTR()); + + currentFrame = SelectionMovementUtils::GetFrameForNodeOffset( + aNode, aContentOffset, aHint, ¤tOffset); + if (!currentFrame) { + return levels; + } + + auto [frameStart, frameEnd] = currentFrame->GetOffsets(); + + if (0 == frameStart && 0 == frameEnd) { + direction = eDirPrevious; + } else if (static_cast(frameStart) == currentOffset) { + direction = eDirPrevious; + } else if (static_cast(frameEnd) == currentOffset) { + direction = eDirNext; + } else { + // we are neither at the beginning nor at the end of the frame, so we have + // no worries + intl::BidiEmbeddingLevel currentLevel = currentFrame->GetEmbeddingLevel(); + levels.SetData(currentFrame, currentFrame, currentLevel, currentLevel); + return levels; + } + + PeekOffsetOptions peekOffsetOptions{PeekOffsetOption::StopAtScroller}; + if (aJumpLines) { + peekOffsetOptions += PeekOffsetOption::JumpLines; + } + nsIFrame* newFrame = + currentFrame->GetFrameFromDirection(direction, peekOffsetOptions).mFrame; + + FrameBidiData currentBidi = currentFrame->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 (currentFrame->IsBrFrame()) { + currentFrame = nullptr; + currentLevel = currentBidi.baseLevel; + } + if (newFrame && newFrame->IsBrFrame()) { + newFrame = nullptr; + newLevel = currentBidi.baseLevel; + } + } + + if (direction == eDirNext) { + levels.SetData(currentFrame, newFrame, currentLevel, newLevel); + } else { + levels.SetData(newFrame, currentFrame, 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 +nsIFrame* SelectionMovementUtils::GetFrameForNodeOffset( + nsIContent* aNode, uint32_t aOffset, CaretAssociationHint aHint, + uint32_t* aReturnOffset /* = nullptr */) { + if (!aNode) { + return nullptr; + } + + if (static_cast(aOffset) < 0) { + return nullptr; + } + + if (!aNode->GetPrimaryFrame() && !IsDisplayContents(aNode)) { + return nullptr; + } + + nsIFrame* returnFrame = nullptr; + nsCOMPtr theNode; + uint32_t offsetInFrameContent; + + while (true) { + 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. + + RefPtr textNode = theNode->GetAsText(); + if (textNode) { + 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 nullptr; + } + + 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 (ShadowRoot* shadow = ShadowRoot::FromNode(theNode)) { + theNode = shadow->GetHost(); + } + + returnFrame = theNode->GetPrimaryFrame(); + if (!returnFrame) { + if (aHint == CaretAssociationHint::Before) { + if (aOffset > 0) { + --aOffset; + continue; + } + break; + } + if (aOffset < theNode->GetChildCount()) { + ++aOffset; + continue; + } + break; + } + + break; + } // end while + + if (!returnFrame) { + return nullptr; + } + + // 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); + if (aReturnOffset) { + *aReturnOffset = offsetInFrameContent; + } + return returnFrame; +} + +/** + * Find the first frame in an in-order traversal of the frame subtree rooted + * at aFrame which is either a text frame logically at the end of a line, + * or which is aStopAtFrame. Return null if no such frame is found. We don't + * descend into the children of non-eLineParticipant frames. + */ +static nsIFrame* CheckForTrailingTextFrameRecursive(nsIFrame* aFrame, + nsIFrame* aStopAtFrame) { + if (aFrame == aStopAtFrame || + ((aFrame->IsTextFrame() && + (static_cast(aFrame))->IsAtEndOfLine()))) { + return aFrame; + } + if (!aFrame->IsLineParticipant()) { + return nullptr; + } + + for (nsIFrame* f : aFrame->PrincipalChildList()) { + if (nsIFrame* r = CheckForTrailingTextFrameRecursive(f, aStopAtFrame)) { + return r; + } + } + return nullptr; +} + +static nsLineBox* FindContainingLine(nsIFrame* aFrame) { + while (aFrame && aFrame->IsLineParticipant()) { + nsIFrame* parent = aFrame->GetParent(); + nsBlockFrame* blockParent = do_QueryFrame(parent); + if (blockParent) { + bool isValid; + nsBlockInFlowLineIterator iter(blockParent, aFrame, &isValid); + return isValid ? iter.GetLine().get() : nullptr; + } + aFrame = parent; + } + return nullptr; +} + +static void AdjustCaretFrameForLineEnd(nsIFrame** aFrame, uint32_t* aOffset, + bool aEditableOnly) { + nsLineBox* line = FindContainingLine(*aFrame); + if (!line) { + return; + } + uint32_t count = line->GetChildCount(); + for (nsIFrame* f = line->mFirstChild; count > 0; + --count, f = f->GetNextSibling()) { + nsIFrame* r = CheckForTrailingTextFrameRecursive(f, *aFrame); + if (r == *aFrame) { + return; + } + if (!r) { + continue; + } + // If found text frame is non-editable but the start frame content is + // editable, we don't want to put caret into the non-editable text node. + // We should return the given frame as-is in this case. + if (aEditableOnly && !r->GetContent()->IsEditable()) { + return; + } + // We found our frame, but we may not be able to properly paint the caret + // if -moz-user-modify differs from our actual frame. + MOZ_ASSERT(r->IsTextFrame(), "Expected text frame"); + *aFrame = r; + *aOffset = (static_cast(r))->GetContentEnd(); + return; + // FYI: Setting the caret association hint was done during a call of + // GetPrimaryFrameForCaretAtFocusNode. Therefore, this may not be intended + // by the original author. + } +} + +CaretFrameData SelectionMovementUtils::GetCaretFrameForNodeOffset( + const nsFrameSelection* aFrameSelection, nsIContent* aContentNode, + uint32_t aOffset, CaretAssociationHint aFrameHint, + intl::BidiEmbeddingLevel aBidiLevel, + ForceEditableRegion aForceEditableRegion) { + if (!aContentNode || !aContentNode->IsInComposedDoc()) { + return {}; + } + + CaretFrameData result; + result.mHint = aFrameHint; + if (aFrameSelection) { + PresShell* presShell = aFrameSelection->GetPresShell(); + if (!presShell) { + return {}; + } + + if (!aContentNode || !aContentNode->IsInComposedDoc() || + presShell->GetDocument() != aContentNode->GetComposedDoc()) { + return {}; + } + + result.mHint = aFrameSelection->GetHint(); + } + + MOZ_ASSERT_IF(aForceEditableRegion == ForceEditableRegion::Yes, + aContentNode->IsEditable()); + + result.mFrame = result.mUnadjustedFrame = + SelectionMovementUtils::GetFrameForNodeOffset( + aContentNode, aOffset, aFrameHint, &result.mOffsetInFrameContent); + if (!result.mFrame) { + return {}; + } + + if (SelectionMovementUtils::AdjustFrameForLineStart( + result.mFrame, result.mOffsetInFrameContent)) { + result.mHint = CaretAssociationHint::After; + } else { + // if the frame is after a text frame that's logically at the end of the + // line (e.g. if the frame is a
frame), then put the caret at the end + // of that text frame instead. This way, the caret will be positioned as if + // trailing whitespace was not trimmed. + AdjustCaretFrameForLineEnd( + &result.mFrame, &result.mOffsetInFrameContent, + aForceEditableRegion == ForceEditableRegion::Yes); + } + + // Mamdouh : modification of the caret to work at rtl and ltr with Bidi + // + // Direction Style from visibility->mDirection + // ------------------ + if (!result.mFrame->PresContext()->BidiEnabled()) { + return result; + } + + // If there has been a reflow, take the caret Bidi level to be the level of + // the current frame + if (aBidiLevel & BIDI_LEVEL_UNDEFINED) { + aBidiLevel = result.mFrame->GetEmbeddingLevel(); + } + + nsIFrame* frameBefore; + nsIFrame* frameAfter; + intl::BidiEmbeddingLevel + levelBefore; // Bidi level of the character before the caret + intl::BidiEmbeddingLevel + levelAfter; // Bidi level of the character after the caret + + auto [start, end] = result.mFrame->GetOffsets(); + if (start == 0 || end == 0 || + static_cast(start) == result.mOffsetInFrameContent || + static_cast(end) == result.mOffsetInFrameContent) { + nsPrevNextBidiLevels levels = SelectionMovementUtils::GetPrevNextBidiLevels( + aContentNode, aOffset, result.mHint, false); + + /* Boundary condition, we need to know the Bidi levels of the characters + * before and after the caret */ + if (levels.mFrameBefore || levels.mFrameAfter) { + frameBefore = levels.mFrameBefore; + frameAfter = levels.mFrameAfter; + levelBefore = levels.mLevelBefore; + levelAfter = levels.mLevelAfter; + + if ((levelBefore != levelAfter) || (aBidiLevel != levelBefore)) { + aBidiLevel = + std::max(aBidiLevel, std::min(levelBefore, levelAfter)); // rule c3 + aBidiLevel = + std::min(aBidiLevel, std::max(levelBefore, levelAfter)); // rule c4 + if (aBidiLevel == levelBefore || // rule c1 + (aBidiLevel > levelBefore && aBidiLevel < levelAfter && + aBidiLevel.IsSameDirection(levelBefore)) || // rule c5 + (aBidiLevel < levelBefore && aBidiLevel > levelAfter && + aBidiLevel.IsSameDirection(levelBefore))) // rule c9 + { + if (result.mFrame != frameBefore) { + if (frameBefore) { // if there is a frameBefore, move into it + result.mFrame = frameBefore; + std::tie(start, end) = result.mFrame->GetOffsets(); + result.mOffsetInFrameContent = end; + } else { + // if there is no frameBefore, we must be at the beginning of + // the line so we stay with the current frame. Exception: when + // the first frame on the line has a different Bidi level from + // the paragraph level, there is no real frame for the caret to + // be in. We have to find the visually first frame on the line. + intl::BidiEmbeddingLevel baseLevel = frameAfter->GetBaseLevel(); + if (baseLevel != levelAfter) { + PeekOffsetStruct pos(eSelectBeginLine, eDirPrevious, 0, + nsPoint(0, 0), + {PeekOffsetOption::StopAtScroller, + PeekOffsetOption::Visual}); + if (NS_SUCCEEDED(frameAfter->PeekOffset(&pos))) { + result.mFrame = pos.mResultFrame; + result.mOffsetInFrameContent = pos.mContentOffset; + } + } + } + } + } else if (aBidiLevel == levelAfter || // rule c2 + (aBidiLevel > levelBefore && aBidiLevel < levelAfter && + aBidiLevel.IsSameDirection(levelAfter)) || // rule c6 + (aBidiLevel < levelBefore && aBidiLevel > levelAfter && + aBidiLevel.IsSameDirection(levelAfter))) // rule c10 + { + if (result.mFrame != frameAfter) { + if (frameAfter) { + // if there is a frameAfter, move into it + result.mFrame = frameAfter; + std::tie(start, end) = result.mFrame->GetOffsets(); + result.mOffsetInFrameContent = start; + } else { + // if there is no frameAfter, we must be at the end of the line + // so we stay with the current frame. + // Exception: when the last frame on the line has a different + // Bidi level from the paragraph level, there is no real frame + // for the caret to be in. We have to find the visually last + // frame on the line. + intl::BidiEmbeddingLevel baseLevel = frameBefore->GetBaseLevel(); + if (baseLevel != levelBefore) { + PeekOffsetStruct pos(eSelectEndLine, eDirNext, 0, nsPoint(0, 0), + {PeekOffsetOption::StopAtScroller, + PeekOffsetOption::Visual}); + if (NS_SUCCEEDED(frameBefore->PeekOffset(&pos))) { + result.mFrame = pos.mResultFrame; + result.mOffsetInFrameContent = pos.mContentOffset; + } + } + } + } + } else if (aBidiLevel > levelBefore && + aBidiLevel < levelAfter && // rule c7/8 + // before and after have the same parity + levelBefore.IsSameDirection(levelAfter) && + // caret has different parity + !aBidiLevel.IsSameDirection(levelAfter)) { + MOZ_ASSERT_IF(aFrameSelection && aFrameSelection->GetPresShell(), + aFrameSelection->GetPresShell()->GetPresContext() == + frameAfter->PresContext()); + Result frameOrError = + SelectionMovementUtils::GetFrameFromLevel(frameAfter, eDirNext, + aBidiLevel); + if (MOZ_LIKELY(frameOrError.isOk())) { + result.mFrame = frameOrError.unwrap(); + std::tie(start, end) = result.mFrame->GetOffsets(); + levelAfter = result.mFrame->GetEmbeddingLevel(); + if (aBidiLevel.IsRTL()) { + // c8: caret to the right of the rightmost character + result.mOffsetInFrameContent = levelAfter.IsRTL() ? start : end; + } else { + // c7: caret to the left of the leftmost character + result.mOffsetInFrameContent = levelAfter.IsRTL() ? end : start; + } + } + } else if (aBidiLevel < levelBefore && + aBidiLevel > levelAfter && // rule c11/12 + // before and after have the same parity + levelBefore.IsSameDirection(levelAfter) && + // caret has different parity + !aBidiLevel.IsSameDirection(levelAfter)) { + MOZ_ASSERT_IF(aFrameSelection && aFrameSelection->GetPresShell(), + aFrameSelection->GetPresShell()->GetPresContext() == + frameBefore->PresContext()); + Result frameOrError = + SelectionMovementUtils::GetFrameFromLevel( + frameBefore, eDirPrevious, aBidiLevel); + if (MOZ_LIKELY(frameOrError.isOk())) { + result.mFrame = frameOrError.unwrap(); + std::tie(start, end) = result.mFrame->GetOffsets(); + levelBefore = result.mFrame->GetEmbeddingLevel(); + if (aBidiLevel.IsRTL()) { + // c12: caret to the left of the leftmost character + result.mOffsetInFrameContent = levelBefore.IsRTL() ? end : start; + } else { + // c11: caret to the right of the rightmost character + result.mOffsetInFrameContent = levelBefore.IsRTL() ? start : end; + } + } + } + } + } + } + + return result; +} + +// static +PrimaryFrameData SelectionMovementUtils::GetPrimaryFrameForCaret( + nsIContent* aContent, uint32_t aOffset, bool aVisual, + CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel) { + MOZ_ASSERT(aContent); + + { + const PrimaryFrameData result = + SelectionMovementUtils::GetPrimaryOrCaretFrameForNodeOffset( + aContent, aOffset, aVisual, aHint, aCaretBidiLevel); + if (result.mFrame) { + return result; + } + } + + // If aContent is whitespace only, we promote focus node to parent because + // whitespace only node might have no frame. + + if (!aContent->TextIsOnlyWhitespace()) { + return {}; + } + + nsIContent* parent = aContent->GetParent(); + if (NS_WARN_IF(!parent)) { + return {}; + } + const Maybe offset = parent->ComputeIndexOf(aContent); + if (NS_WARN_IF(offset.isNothing())) { + return {}; + } + return SelectionMovementUtils::GetPrimaryOrCaretFrameForNodeOffset( + parent, *offset, aVisual, aHint, aCaretBidiLevel); +} + +// static +PrimaryFrameData SelectionMovementUtils::GetPrimaryOrCaretFrameForNodeOffset( + nsIContent* aContent, uint32_t aOffset, bool aVisual, + CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel) { + if (aVisual) { + const CaretFrameData result = + SelectionMovementUtils::GetCaretFrameForNodeOffset( + nullptr, aContent, aOffset, aHint, aCaretBidiLevel, + aContent && aContent->IsEditable() ? ForceEditableRegion::Yes + : ForceEditableRegion::No); + return {result.mFrame, result.mOffsetInFrameContent, result.mHint}; + } + + uint32_t offset = 0; + nsIFrame* theFrame = SelectionMovementUtils::GetFrameForNodeOffset( + aContent, aOffset, aHint, &offset); + return {theFrame, offset, aHint}; +} + +} // namespace mozilla diff --git a/layout/generic/SelectionMovementUtils.h b/layout/generic/SelectionMovementUtils.h new file mode 100644 index 000000000000..5d8194948815 --- /dev/null +++ b/layout/generic/SelectionMovementUtils.h @@ -0,0 +1,177 @@ +/* -*- 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/. */ + +#ifndef mozilla_SelectionMovementUtils_h +#define mozilla_SelectionMovementUtils_h + +#include "mozilla/intl/BidiEmbeddingLevel.h" +#include "mozilla/Attributes.h" +#include "mozilla/EnumSet.h" +#include "mozilla/Result.h" +#include "nsIFrame.h" + +struct nsPrevNextBidiLevels; + +namespace mozilla { + +class PresShell; +enum class PeekOffsetOption : uint16_t; + +namespace intl { +class BidiEmbeddingLevel; +} + +struct MOZ_STACK_CLASS PrimaryFrameData { + // The frame which should be used to layout the caret. + nsIFrame* mFrame = nullptr; + // The offset in content of mFrame. This is valid only when mFrame is not + // nullptr. + uint32_t mOffsetInFrameContent = 0; + // Whether the caret should be put before or after the point. This is valid + // only when mFrame is not nullptr. + CaretAssociationHint mHint{0}; // Before +}; + +struct MOZ_STACK_CLASS CaretFrameData : public PrimaryFrameData { + // The frame which is found only from a DOM point. This frame becomes + // different from mFrame when the point is around end of a line or + // at a bidi text boundary. + nsIFrame* mUnadjustedFrame = nullptr; +}; + +enum class ForceEditableRegion : bool { No, Yes }; + +class SelectionMovementUtils final { + public: + using PeekOffsetOptions = EnumSet; + + /** + * Given a node and its child offset, return the nsIFrame and the offset into + * that frame. + * + * @param aNode input parameter for the node to look at + * TODO: Make this `const nsIContent*` for `ContentEventHandler`. + * @param aOffset offset into above node. + * @param aReturnOffset will contain offset into frame. + */ + static nsIFrame* GetFrameForNodeOffset(nsIContent* aNode, uint32_t aOffset, + CaretAssociationHint aHint, + uint32_t* aReturnOffset = nullptr); + + /** + * GetPrevNextBidiLevels will return the frames and associated Bidi levels of + * the characters logically before and after a (collapsed) selection. + * + * @param aNode is the node containing the selection + * @param aContentOffset is the offset of the selection in the node + * @param aJumpLines + * If true, look across line boundaries. + * If false, behave as if there were base-level frames at line edges. + * + * @return A struct holding the before/after frame and the before/after + * level. + * + * At the beginning and end of each line there is assumed to be a frame with + * Bidi level equal to the paragraph embedding level. + * + * In these cases the before frame and after frame respectively will be + * nullptr. + */ + static nsPrevNextBidiLevels GetPrevNextBidiLevels(nsIContent* aNode, + uint32_t aContentOffset, + CaretAssociationHint aHint, + bool aJumpLines); + + /** + * PeekOffsetForCaretMove() only peek offset for caret move from the specified + * point of the normal selection. I.e., won't change selection ranges nor + * bidi information. + */ + static Result PeekOffsetForCaretMove( + nsIContent* aContent, uint32_t aOffset, nsDirection aDirection, + CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel, + const nsSelectionAmount aAmount, const nsPoint& aDesiredCaretPos, + PeekOffsetOptions aOptions); + + /** + * IsIntraLineCaretMove() is a helper method for PeekOffsetForCaretMove() + * and CreateRangeExtendedToSomwhereFromNormalSelection(). This returns + * whether aAmount is intra line move or is crossing hard line break. + * This returns error if aMount is not supported by the methods. + */ + static Result IsIntraLineCaretMove( + nsSelectionAmount aAmount) { + switch (aAmount) { + case eSelectCharacter: + case eSelectCluster: + case eSelectWord: + case eSelectWordNoSpace: + case eSelectBeginLine: + case eSelectEndLine: + return true; + case eSelectLine: + return false; + default: + return Err(NS_ERROR_FAILURE); + } + } + + /** + * Return a frame for considering caret geometry. + * + * @param aFrameSelection [optional] If this is specified and selection in + * aContent is not managed by the specified + * instance, return nullptr. + * @param aContentNode The content node where selection is collapsed. + * @param aOffset Collapsed position in aContentNode + * @param aFrameHint Caret association hint. + * @param aBidiLevel + * @param aForceEditableRegion Whether selection should be limited in + * editable region or not. + */ + static CaretFrameData GetCaretFrameForNodeOffset( + const nsFrameSelection* aFrameSelection, nsIContent* aContentNode, + uint32_t aOffset, CaretAssociationHint aFrameHint, + intl::BidiEmbeddingLevel aBidiLevel, + ForceEditableRegion aForceEditableRegion); + + static bool AdjustFrameForLineStart(nsIFrame*& aFrame, + uint32_t& aFrameOffset); + + /** + * Get primary frame and some other data for putting caret or extending + * selection at the point. + */ + static PrimaryFrameData GetPrimaryFrameForCaret( + nsIContent* aContent, uint32_t aOffset, bool aVisual, + CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel); + + private: + /** + * GetFrameFromLevel will scan in a given direction + * until it finds a frame with a Bidi level less than or equal to a given + * level. It will return the last frame before this. + * + * @param aPresContext is the context to use + * @param aFrameIn is the frame to start from + * @param aDirection is the direction to scan + * @param aBidiLevel is the level to search for + */ + static Result GetFrameFromLevel( + nsIFrame* aFrameIn, nsDirection aDirection, + intl::BidiEmbeddingLevel aBidiLevel); + + // This is helper method for GetPrimaryFrameForCaret. + // If aVisual is true, this returns caret frame. + // If false, this returns primary frame. + static PrimaryFrameData GetPrimaryOrCaretFrameForNodeOffset( + nsIContent* aContent, uint32_t aOffset, bool aVisual, + CaretAssociationHint aHint, intl::BidiEmbeddingLevel aCaretBidiLevel); +}; + +} // namespace mozilla + +#endif // #ifndef mozilla_SelectionMovementUtils_h diff --git a/layout/generic/moz.build b/layout/generic/moz.build index 41471208114b..15fdd5353c68 100644 --- a/layout/generic/moz.build +++ b/layout/generic/moz.build @@ -153,6 +153,7 @@ EXPORTS.mozilla += [ "ScrollPositionUpdate.h", "ScrollSnapInfo.h", "ScrollSnapTargetId.h", + "SelectionMovementUtils.h", "ViewportFrame.h", "WritingModes.h", ] @@ -227,6 +228,7 @@ UNIFIED_SOURCES += [ "ScrollSnap.cpp", "ScrollSnapInfo.cpp", "ScrollVelocityQueue.cpp", + "SelectionMovementUtils.cpp", "StickyScrollContainer.cpp", "ViewportFrame.cpp", "WBRFrame.cpp", diff --git a/layout/generic/nsFrameSelection.cpp b/layout/generic/nsFrameSelection.cpp index 1d9a3114af3b..002d26c4fab6 100644 --- a/layout/generic/nsFrameSelection.cpp +++ b/layout/generic/nsFrameSelection.cpp @@ -86,6 +86,8 @@ #include "nsFocusManager.h" #include "nsPIDOMWindow.h" +#include "SelectionMovementUtils.h" + using namespace mozilla; using namespace mozilla::dom; @@ -755,7 +757,7 @@ nsresult nsFrameSelection::MoveCaret(nsDirection aDirection, bool visualMovement = mCaret.IsVisualMovement(aContinueSelection, aMovementStyle); - const Selection::PrimaryFrameData frameForFocus = + const PrimaryFrameData frameForFocus = sel->GetPrimaryFrameForCaretAtFocusNode(visualMovement); if (!frameForFocus.mFrame) { return NS_ERROR_FAILURE; @@ -766,7 +768,8 @@ nsresult nsFrameSelection::MoveCaret(nsDirection aDirection, SetHint(frameForFocus.mHint); } - Result isIntraLineCaretMove = IsIntraLineCaretMove(aAmount); + Result isIntraLineCaretMove = + SelectionMovementUtils::IsIntraLineCaretMove(aAmount); nsDirection direction{aDirection}; if (isIntraLineCaretMove.isErr()) { return isIntraLineCaretMove.unwrapErr(); @@ -812,7 +815,7 @@ nsresult nsFrameSelection::MoveCaret(nsDirection aDirection, if (result.isOk() && result.inspect().mResultContent) { const PeekOffsetStruct& pos = result.inspect(); nsIFrame* theFrame; - int32_t currentOffset, frameStart, frameEnd; + int32_t frameStart, frameEnd; if (aAmount <= eSelectWordNoSpace) { // For left/right, PeekOffset() sets pos.mResultFrame correctly, but does @@ -822,8 +825,7 @@ nsresult nsFrameSelection::MoveCaret(nsDirection aDirection, // at the end of this frame, not at the beginning of the next one. theFrame = pos.mResultFrame; std::tie(frameStart, frameEnd) = theFrame->GetOffsets(); - currentOffset = pos.mContentOffset; - if (frameEnd == currentOffset && !(frameStart == 0 && frameEnd == 0)) + if (frameEnd == pos.mContentOffset && !(frameStart == 0 && frameEnd == 0)) tHint = CaretAssociationHint::Before; else tHint = CaretAssociationHint::After; @@ -832,8 +834,8 @@ nsresult nsFrameSelection::MoveCaret(nsDirection aDirection, // or not at all. In these cases, get the frame based on the content and // hint returned by PeekOffset(). tHint = pos.mAttach; - theFrame = GetFrameForNodeOffset(pos.mResultContent, pos.mContentOffset, - tHint, ¤tOffset); + theFrame = SelectionMovementUtils::GetFrameForNodeOffset( + pos.mResultContent, pos.mContentOffset, tHint); if (!theFrame) return NS_ERROR_FAILURE; std::tie(frameStart, frameEnd) = theFrame->GetOffsets(); @@ -939,147 +941,15 @@ Result nsFrameSelection::PeekOffsetForCaretMove( options += PeekOffsetOption::ForceEditableRegion; } - return nsFrameSelection::PeekOffsetForCaretMove( + return SelectionMovementUtils::PeekOffsetForCaretMove( content, selection->FocusOffset(), aDirection, GetHint(), GetCaretBidiLevel(), aAmount, aDesiredCaretPos, options); } -// static -mozilla::Result -nsFrameSelection::PeekOffsetForCaretMove( - nsIContent* aContent, uint32_t aOffset, nsDirection aDirection, - CaretAssociationHint aHint, - mozilla::intl::BidiEmbeddingLevel aCaretBidiLevel, - const nsSelectionAmount aAmount, const nsPoint& aDesiredCaretPos, - mozilla::PeekOffsetOptions aOptions) { - const Selection::PrimaryFrameData frameForFocus = - Selection::GetPrimaryFrameForCaret( - aContent, aOffset, aOptions.contains(PeekOffsetOption::Visual), aHint, - aCaretBidiLevel); - if (!frameForFocus.mFrame) { - return Err(NS_ERROR_FAILURE); - } - - aOptions += {PeekOffsetOption::JumpLines, PeekOffsetOption::IsKeyboardSelect}; - PeekOffsetStruct pos( - aAmount, aDirection, - static_cast(frameForFocus.mOffsetInFrameContent), - aDesiredCaretPos, aOptions); - nsresult rv = frameForFocus.mFrame->PeekOffset(&pos); - if (NS_FAILED(rv)) { - return Err(rv); - } - return pos; -} - nsPrevNextBidiLevels nsFrameSelection::GetPrevNextBidiLevels( nsIContent* aNode, uint32_t aContentOffset, bool aJumpLines) const { - return GetPrevNextBidiLevels(aNode, aContentOffset, mCaret.mHint, aJumpLines); -} - -// static -nsPrevNextBidiLevels nsFrameSelection::GetPrevNextBidiLevels( - nsIContent* aNode, uint32_t aContentOffset, CaretAssociationHint aHint, - bool aJumpLines) { - // Get the level of the frames on each side - nsIFrame* currentFrame; - int32_t currentOffset; - nsDirection direction; - - nsPrevNextBidiLevels levels{}; - levels.SetData(nullptr, nullptr, mozilla::intl::BidiEmbeddingLevel::LTR(), - mozilla::intl::BidiEmbeddingLevel::LTR()); - - currentFrame = GetFrameForNodeOffset( - aNode, static_cast(aContentOffset), aHint, ¤tOffset); - if (!currentFrame) { - return levels; - } - - auto [frameStart, frameEnd] = currentFrame->GetOffsets(); - - if (0 == frameStart && 0 == frameEnd) { - direction = eDirPrevious; - } else if (frameStart == currentOffset) { - direction = eDirPrevious; - } else if (frameEnd == currentOffset) { - direction = eDirNext; - } else { - // we are neither at the beginning nor at the end of the frame, so we have - // no worries - mozilla::intl::BidiEmbeddingLevel currentLevel = - currentFrame->GetEmbeddingLevel(); - levels.SetData(currentFrame, currentFrame, currentLevel, currentLevel); - return levels; - } - - PeekOffsetOptions peekOffsetOptions{PeekOffsetOption::StopAtScroller}; - if (aJumpLines) { - peekOffsetOptions += PeekOffsetOption::JumpLines; - } - nsIFrame* newFrame = - currentFrame->GetFrameFromDirection(direction, peekOffsetOptions).mFrame; - - FrameBidiData currentBidi = currentFrame->GetBidiData(); - mozilla::intl::BidiEmbeddingLevel currentLevel = currentBidi.embeddingLevel; - mozilla::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 (currentFrame->IsBrFrame()) { - currentFrame = nullptr; - currentLevel = currentBidi.baseLevel; - } - if (newFrame && newFrame->IsBrFrame()) { - newFrame = nullptr; - newLevel = currentBidi.baseLevel; - } - } - - if (direction == eDirNext) - levels.SetData(currentFrame, newFrame, currentLevel, newLevel); - else - levels.SetData(newFrame, currentFrame, newLevel, currentLevel); - - return levels; -} - -// static -Result nsFrameSelection::GetFrameFromLevel( - nsIFrame* aFrameIn, nsDirection aDirection, - mozilla::intl::BidiEmbeddingLevel aBidiLevel) { - if (!aFrameIn) { - return Err(NS_ERROR_NULL_POINTER); - } - - mozilla::intl::BidiEmbeddingLevel foundLevel = - mozilla::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; + return SelectionMovementUtils::GetPrevNextBidiLevels( + aNode, aContentOffset, mCaret.mHint, aJumpLines); } nsresult nsFrameSelection::MaintainSelection(nsSelectionAmount aAmount) { @@ -1109,7 +979,8 @@ void nsFrameSelection::BidiLevelFromMove(PresShell* aPresShell, case eSelectEndLine: case eSelectNoAmount: { nsPrevNextBidiLevels levels = - GetPrevNextBidiLevels(aNode, aContentOffset, aHint, false); + SelectionMovementUtils::GetPrevNextBidiLevels(aNode, aContentOffset, + aHint, false); SetCaretBidiLevelAndMaybeSchedulePaint( aHint == CaretAssociationHint::Before ? levels.mLevelBefore @@ -1133,10 +1004,8 @@ void nsFrameSelection::BidiLevelFromMove(PresShell* aPresShell, void nsFrameSelection::BidiLevelFromClick(nsIContent* aNode, uint32_t aContentOffset) { nsIFrame* clickInFrame = nullptr; - int32_t OffsetNotUsed; - - clickInFrame = GetFrameForNodeOffset(aNode, aContentOffset, mCaret.mHint, - &OffsetNotUsed); + clickInFrame = SelectionMovementUtils::GetFrameForNodeOffset( + aNode, aContentOffset, mCaret.mHint); if (!clickInFrame) return; SetCaretBidiLevelAndMaybeSchedulePaint(clickInFrame->GetEmbeddingLevel()); @@ -1212,10 +1081,10 @@ void nsFrameSelection::MaintainedRange::AdjustContentOffsets( amount = eSelectEndLine; } - int32_t offset; - nsIFrame* frame = - GetFrameForNodeOffset(aOffsets.content, aOffsets.offset, - CaretAssociationHint::After, &offset); + uint32_t offset; + nsIFrame* frame = SelectionMovementUtils::GetFrameForNodeOffset( + aOffsets.content, aOffsets.offset, CaretAssociationHint::After, + &offset); PeekOffsetOptions peekOffsetOptions{}; if (aStopAtScroller == StopAtScroller::Yes) { @@ -1224,16 +1093,17 @@ void nsFrameSelection::MaintainedRange::AdjustContentOffsets( if (frame && amount == eSelectWord && direction == eDirPrevious) { // To avoid selecting the previous word when at start of word, // first move one character forward. - PeekOffsetStruct charPos(eSelectCharacter, eDirNext, offset, - nsPoint(0, 0), peekOffsetOptions); + PeekOffsetStruct charPos(eSelectCharacter, eDirNext, + static_cast(offset), nsPoint(0, 0), + peekOffsetOptions); if (NS_SUCCEEDED(frame->PeekOffset(&charPos))) { frame = charPos.mResultFrame; offset = charPos.mContentOffset; } } - PeekOffsetStruct pos(amount, direction, offset, nsPoint(0, 0), - peekOffsetOptions); + PeekOffsetStruct pos(amount, direction, static_cast(offset), + nsPoint(0, 0), peekOffsetOptions); if (frame && NS_SUCCEEDED(frame->PeekOffset(&pos)) && pos.mResultContent) { aOffsets.content = pos.mResultContent; aOffsets.offset = pos.mContentOffset; @@ -1760,187 +1630,6 @@ nsresult nsFrameSelection::RepaintSelection(SelectionType aSelectionType) { return mDomSelections[index]->Repaint(mPresShell->GetPresContext()); } -static bool IsDisplayContents(const nsIContent* aContent) { - return aContent->IsElement() && aContent->AsElement()->IsDisplayContents(); -} - -bool nsFrameSelection::AdjustFrameForLineStart(nsIFrame*& aFrame, - int32_t& aFrameOffset) { - if (!aFrame->HasSignificantTerminalNewline()) { - return false; - } - - auto [start, end] = aFrame->GetOffsets(); - if (aFrameOffset != end) { - return false; - } - - nsIFrame* nextSibling = aFrame->GetNextSibling(); - if (!nextSibling) { - return false; - } - - aFrame = nextSibling; - std::tie(start, end) = aFrame->GetOffsets(); - aFrameOffset = start; - return true; -} - -// static -nsIFrame* nsFrameSelection::GetFrameForNodeOffset(nsIContent* aNode, - int32_t aOffset, - CaretAssociationHint aHint, - int32_t* aReturnOffset) { - if (!aNode || !aReturnOffset) return nullptr; - - if (aOffset < 0) return nullptr; - - if (!aNode->GetPrimaryFrame() && !IsDisplayContents(aNode)) { - return nullptr; - } - - nsIFrame* returnFrame = nullptr; - nsCOMPtr theNode; - - while (true) { - *aReturnOffset = aOffset; - - theNode = aNode; - - if (aNode->IsElement()) { - int32_t childIndex = 0; - int32_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; - } else { - // Check to see if theNode is a text node. If it is, translate - // aOffset into an offset into the text node. - - RefPtr textNode = theNode->GetAsText(); - if (textNode) { - if (theNode->GetPrimaryFrame()) { - if (aOffset > childIndex) { - uint32_t textLength = textNode->Length(); - - *aReturnOffset = (int32_t)textLength; - } else { - *aReturnOffset = 0; - } - } else { - int32_t numChildren = aNode->GetChildCount(); - int32_t newChildIndex = aHint == CaretAssociationHint::Before - ? childIndex - 1 - : childIndex + 1; - - if (newChildIndex >= 0 && newChildIndex < numChildren) { - nsCOMPtr newChildNode = - aNode->GetChildAt_Deprecated(newChildIndex); - if (!newChildNode) { - return nullptr; - } - - aNode = newChildNode; - aOffset = aHint == CaretAssociationHint::Before - ? aNode->GetChildCount() - : 0; - continue; - } else { - // 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 (ShadowRoot* shadow = ShadowRoot::FromNode(theNode)) { - theNode = shadow->GetHost(); - } - - returnFrame = theNode->GetPrimaryFrame(); - if (!returnFrame) { - if (aHint == CaretAssociationHint::Before) { - if (aOffset > 0) { - --aOffset; - continue; - } else { - break; - } - } else { - int32_t end = theNode->GetChildCount(); - if (aOffset < end) { - ++aOffset; - continue; - } else { - break; - } - } - } - - break; - } // end while - - if (!returnFrame) return nullptr; - - // 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; - *aReturnOffset = 0; - } - } - - // find the child frame containing the offset we want - returnFrame->GetChildFrameContainingOffset( - *aReturnOffset, aHint == CaretAssociationHint::After, &aOffset, - &returnFrame); - return returnFrame; -} - nsIFrame* nsFrameSelection::GetFrameToPageSelect() const { if (NS_WARN_IF(!mPresShell)) { return nullptr; @@ -2180,7 +1869,7 @@ nsresult nsFrameSelection::PhysicalMove(int16_t aDirection, int16_t aAmount, {eDirNext, blockNextAmount}}; WritingMode wm; - const Selection::PrimaryFrameData frameForFocus = + const PrimaryFrameData frameForFocus = sel->GetPrimaryFrameForCaretAtFocusNode(true); if (frameForFocus.mFrame) { // FYI: Setting the caret association hint was done during a call of diff --git a/layout/generic/nsFrameSelection.h b/layout/generic/nsFrameSelection.h index 6c6450036db1..e8c55d53b56a 100644 --- a/layout/generic/nsFrameSelection.h +++ b/layout/generic/nsFrameSelection.h @@ -485,21 +485,6 @@ class nsFrameSelection final { bool IsValidSelectionPoint(nsINode* aNode) const; - static bool AdjustFrameForLineStart(nsIFrame*& aFrame, int32_t& aFrameOffset); - - /** - * Given a node and its child offset, return the nsIFrame and the offset into - * that frame. - * - * @param aNode input parameter for the node to look at - * TODO: Make this `const nsIContent*` for `ContentEventHandler`. - * @param aOffset offset into above node. - * @param aReturnOffset will contain offset into frame. - */ - static nsIFrame* GetFrameForNodeOffset(nsIContent* aNode, int32_t aOffset, - CaretAssociationHint aHint, - int32_t* aReturnOffset); - /** * GetFrameToPageSelect() returns a frame which is ancestor limit of * per-page selection. The frame may not be scrollable. E.g., @@ -740,24 +725,6 @@ class nsFrameSelection final { nsPrevNextBidiLevels GetPrevNextBidiLevels(nsIContent* aNode, uint32_t aContentOffset, bool aJumpLines) const; - static nsPrevNextBidiLevels GetPrevNextBidiLevels(nsIContent* aNode, - uint32_t aContentOffset, - CaretAssociationHint aHint, - bool aJumpLines); - - /** - * GetFrameFromLevel will scan in a given direction - * until it finds a frame with a Bidi level less than or equal to a given - * level. It will return the last frame before this. - * - * @param aPresContext is the context to use - * @param aFrameIn is the frame to start from - * @param aDirection is the direction to scan - * @param aBidiLevel is the level to search for - */ - static mozilla::Result GetFrameFromLevel( - nsIFrame* aFrameIn, nsDirection aDirection, - mozilla::intl::BidiEmbeddingLevel aBidiLevel); /** * MaintainSelection will track the normal selection as being "sticky". @@ -916,19 +883,6 @@ class nsFrameSelection final { const nsSelectionAmount aAmount, CaretMovementStyle aMovementStyle, const nsPoint& aDesiredCaretPos) const; - /** - * PeekOffsetForCaretMove() only peek offset for caret move from the specified - * point of the normal selection. I.e., won't change selection ranges nor - * bidi information. - */ - static mozilla::Result - PeekOffsetForCaretMove(nsIContent* aContent, uint32_t aOffset, - nsDirection aDirection, CaretAssociationHint aHint, - mozilla::intl::BidiEmbeddingLevel aCaretBidiLevel, - const nsSelectionAmount aAmount, - const nsPoint& aDesiredCaretPos, - mozilla::PeekOffsetOptions aOptions); - /** * CreateRangeExtendedToSomewhere() is common method to implement * CreateRangeExtendedTo*(). This method creates a range extended from @@ -940,29 +894,6 @@ class nsFrameSelection final { const nsSelectionAmount aAmount, CaretMovementStyle aMovementStyle); - /** - * IsIntraLineCaretMove() is a helper method for PeekOffsetForCaretMove() - * and CreateRangeExtendedToSomwhereFromNormalSelection(). This returns - * whether aAmount is intra line move or is crossing hard line break. - * This returns error if aMount is not supported by the methods. - */ - static mozilla::Result IsIntraLineCaretMove( - nsSelectionAmount aAmount) { - switch (aAmount) { - case eSelectCharacter: - case eSelectCluster: - case eSelectWord: - case eSelectWordNoSpace: - case eSelectBeginLine: - case eSelectEndLine: - return true; - case eSelectLine: - return false; - default: - return mozilla::Err(NS_ERROR_FAILURE); - } - } - void InvalidateDesiredCaretPos(); // do not listen to mDesiredCaretPos.mValue // you must get another. diff --git a/layout/generic/nsIFrame.cpp b/layout/generic/nsIFrame.cpp index 2b4cc137e41d..b55bf4e55dce 100644 --- a/layout/generic/nsIFrame.cpp +++ b/layout/generic/nsIFrame.cpp @@ -35,6 +35,7 @@ #include "mozilla/PresShell.h" #include "mozilla/PresShellInlines.h" #include "mozilla/ResultExtensions.h" +#include "mozilla/SelectionMovementUtils.h" #include "mozilla/Sprintf.h" #include "mozilla/StaticAnalysisFunctions.h" #include "mozilla/StaticPrefs_layout.h" @@ -4939,15 +4940,15 @@ nsresult nsIFrame::SelectByTypeAtPoint(nsPresContext* aPresContext, return NS_ERROR_FAILURE; } - int32_t offset; - nsIFrame* frame = nsFrameSelection::GetFrameForNodeOffset( + uint32_t offset; + nsIFrame* frame = SelectionMovementUtils::GetFrameForNodeOffset( offsets.content, offsets.offset, offsets.associate, &offset); if (!frame) { return NS_ERROR_FAILURE; } - return frame->PeekBackwardAndForward(aBeginAmountType, aEndAmountType, offset, - aBeginAmountType != eSelectWord, - aSelectFlags); + return frame->PeekBackwardAndForward( + aBeginAmountType, aEndAmountType, static_cast(offset), + aBeginAmountType != eSelectWord, aSelectFlags); } /**