tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit c6a0e55250831f5322760be73def174222a65943
parent 15a71a88daf16d802f2f4f0f67676e94a4d35ac1
Author: Masayuki Nakano <masayuki@d-toybox.com>
Date:   Mon, 15 Dec 2025 07:50:40 +0000

Bug 2000978 - Rewrite `AccessibleCaretManager::GetFrameForRange(Start|End)` r=TYLin,layout-reviewers

This patch makes `AccessibleCaretManager::GetFrameForRangeStart()` and
`AccessibleCaretManager::GetFrameForRangeEnd()` use the new utility
methods which are designed for them.

These changes will fix the following cases:
* the first/last container element in the range is completely invisible
* the first/last leaf in the range is wrapped in an unselectable element

These changes won't fix the following issue:
* not supporting ranges across shadow DOM boundaries (bug 1607497)
* not properly showing the carets if first/last range is collapsed

Differential Revision: https://phabricator.services.mozilla.com/D274094

Diffstat:
Mlayout/base/AccessibleCaretManager.cpp | 300++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mlayout/base/AccessibleCaretManager.h | 69+++++++++++++++++++++++++++++++++++++++------------------------------
Mlayout/generic/SelectionMovementUtils.cpp | 4++--
Mlayout/generic/SelectionMovementUtils.h | 13+++++++++----
Atesting/web-platform/meta/selection/shadow-dom/cross-shadow-boundary-1.html.ini | 2++
Atesting/web-platform/mozilla/meta/selection/select-text-and-invisible-br.html.ini | 2++
Atesting/web-platform/mozilla/meta/selection/select-unselectable-img.html.ini | 2++
Atesting/web-platform/mozilla/meta/selection/select-unselectable-span-1.html.ini | 3+++
Atesting/web-platform/mozilla/meta/selection/select-unselectable-span-2.html.ini | 2++
Atesting/web-platform/mozilla/meta/selection/select-unselectable-text.html.ini | 2++
Atesting/web-platform/mozilla/tests/selection/select-text-and-invisible-br.html | 13+++++++++++++
Atesting/web-platform/mozilla/tests/selection/select-text-ref.html | 12++++++++++++
Atesting/web-platform/mozilla/tests/selection/select-unselectable-img-ref.html | 21+++++++++++++++++++++
Atesting/web-platform/mozilla/tests/selection/select-unselectable-img.html | 16++++++++++++++++
Atesting/web-platform/mozilla/tests/selection/select-unselectable-span-1-ref.html | 13+++++++++++++
Atesting/web-platform/mozilla/tests/selection/select-unselectable-span-1.html | 14++++++++++++++
Atesting/web-platform/mozilla/tests/selection/select-unselectable-span-2-ref.html | 19+++++++++++++++++++
Atesting/web-platform/mozilla/tests/selection/select-unselectable-span-2.html | 14++++++++++++++
Atesting/web-platform/mozilla/tests/selection/select-unselectable-text-ref.html | 18++++++++++++++++++
Atesting/web-platform/mozilla/tests/selection/select-unselectable-text.html | 13+++++++++++++
Mtesting/web-platform/tests/css/css-scroll-anchoring/contenteditable-near-cursor.tentative.html | 3++-
21 files changed, 384 insertions(+), 171 deletions(-)

diff --git a/layout/base/AccessibleCaretManager.cpp b/layout/base/AccessibleCaretManager.cpp @@ -310,18 +310,22 @@ void AccessibleCaretManager::UpdateCaretsForSelectionMode( // MockAccessibleCaretManager() to make it always return true. Therefore, // we cannot return earlier when mPresShell is nullptr or the result of // GetFrameForRangeStart() is nullptr. - int32_t startOffset = 0; - nsIFrame* startFrame = - mPresShell ? GetFrameForRangeStart(*GetSelection()->GetFirstRange(), - &startOffset) - : nullptr; - int32_t endOffset = 0; - nsIFrame* endFrame = + const FrameAndOffset startFrameAndOffset = + mPresShell ? GetFirstVisibleLeafFrameOrUnselectableChildFrame( + *GetSelection()->GetFirstRange()) + : FrameAndOffset{}; + nsCOMPtr<nsIContent> endContent; + const FrameAndOffset endFrameAndOffset = mPresShell - ? GetFrameForRangeEnd(*GetSelection()->GetLastRange(), &endOffset) - : nullptr; - - if (!CompareTreePosition(startFrame, startOffset, endFrame, endOffset)) { + ? GetLastVisibleLeafFrameOrUnselectableChildFrame( + *GetSelection()->GetLastRange(), getter_AddRefs(endContent)) + : FrameAndOffset{}; + + if (!CompareTreePosition( + startFrameAndOffset.mFrame, + static_cast<int32_t>(startFrameAndOffset.mOffsetInFrameContent), + endFrameAndOffset.mFrame, + static_cast<int32_t>(endFrameAndOffset.mOffsetInFrameContent))) { // XXX: Do we really have to hide carets if this condition isn't satisfied? HideCaretsAndDispatchCaretStateChangedEvent(); return; @@ -347,10 +351,22 @@ void AccessibleCaretManager::UpdateCaretsForSelectionMode( return result; }; - PositionChangedResult firstCaretResult = - updateSingleCaret(mCarets.GetFirst(), startFrame, startOffset); + PositionChangedResult firstCaretResult = updateSingleCaret( + mCarets.GetFirst(), startFrameAndOffset.mFrame, + static_cast<int32_t>(startFrameAndOffset.mOffsetInFrameContent)); + // If we get a frame for a child node for the end boundary, e.g., when the + // last visible content is <img> or something or unselectable container, + // we want to put the second caret to next to its end edge. Then, we use + // the specific behavior of nsIFrame::GetPointFromOffset() (called by + // nsCaret::GetGeometryForFrame() in AccessibleCaret::SetPosition()) which + // returns the end edge if we set the length of frame content + 1. + const uint32_t offsetInEndFrameContent = + endFrameAndOffset.GetFrameContent() == endContent + ? endFrameAndOffset.mOffsetInFrameContent + : endFrameAndOffset.GetFrameContent()->Length() + 1; PositionChangedResult secondCaretResult = - updateSingleCaret(mCarets.GetSecond(), endFrame, endOffset); + updateSingleCaret(mCarets.GetSecond(), endFrameAndOffset.mFrame, + static_cast<int32_t>(offsetInEndFrameContent)); mIsCaretPositionChanged = firstCaretResult == PositionChangedResult::Position || @@ -368,7 +384,8 @@ void AccessibleCaretManager::UpdateCaretsForSelectionMode( // old appearance. Otherwise we might override the appearance set by the // caller. if (StaticPrefs::layout_accessiblecaret_always_tilt()) { - UpdateCaretsForAlwaysTilt(startFrame, endFrame); + UpdateCaretsForAlwaysTilt(startFrameAndOffset.mFrame, + endFrameAndOffset.mFrame); } else { UpdateCaretsForOverlappingTilt(); } @@ -1042,133 +1059,147 @@ void AccessibleCaretManager::LayoutFlusher::MaybeFlush( } } -nsIFrame* AccessibleCaretManager::GetFrameForRangeStart( - nsRange& aRange, int32_t* aOutOffsetInFrameContent, - nsIContent** aOutContent /* = nullptr */, +static nsIFrame* GetChildFrameContainingOffset( + nsIFrame* aChildFrame, uint32_t aOffsetInChildFrameContent, + CaretAssociationHint aHint) { + nsIFrame* frameAtOffset = nullptr; + int32_t unused = 0; + if (NS_WARN_IF(NS_FAILED(aChildFrame->GetChildFrameContainingOffset( + static_cast<int32_t>(aOffsetInChildFrameContent), + aHint == CaretAssociationHint::After, &unused, &frameAtOffset)))) { + frameAtOffset = aChildFrame; + } + return frameAtOffset; +} + +FrameAndOffset +AccessibleCaretManager::GetFirstVisibleLeafFrameOrUnselectableChildFrame( + nsRange& aRange, nsIContent** aOutContent /* = nullptr */, int32_t* aOutOffsetInContent /* = nullptr */) const { if (!mPresShell) { - return nullptr; + return {}; } MOZ_ASSERT(GetCaretMode() == CaretMode::Selection); - MOZ_ASSERT(aOutOffsetInFrameContent, - "aOutOffsetInFrameContent shouldn't be nullptr!"); - - nsIContent* const startContent = - nsIContent::FromNodeOrNull(aRange.GetStartContainer()); - if (startContent && startContent->IsSelectable()) { - // FIXME: GetFrameForNodeOffset() may return unselectable frame. It should - // have an option to stop digging it if it reaches unselectable child. - uint32_t outOffset = 0; - nsIFrame* const startFrame = SelectionMovementUtils::GetFrameForNodeOffset( - startContent, aRange.StartOffset(), CaretAssociationHint::After, - &outOffset); - if (startFrame) { - MOZ_DIAGNOSTIC_ASSERT(startFrame->GetContent()); - *aOutOffsetInFrameContent = static_cast<int32_t>(outOffset); - if (aOutContent) { - *aOutContent = do_AddRef(startContent).take(); - } - if (aOutOffsetInContent) { - *aOutOffsetInContent = static_cast<int32_t>(aRange.StartOffset()); - } - return startFrame; - } + + // FYI: aRange may be collapsed if `Selection` has multiple ranges. + if (MOZ_UNLIKELY(aRange.Collapsed())) { + return {}; } - UnsafePreContentIterator iter; - if (NS_WARN_IF(NS_FAILED(iter.Init(&aRange)))) { - return nullptr; + const RawRangeBoundary& shrunkenStart = + SelectionMovementUtils::GetFirstVisiblePointAtLeaf(aRange); + if (MOZ_UNLIKELY(!shrunkenStart.IsSet())) { + return {}; } - for (; !iter.IsDone(); iter.Next()) { - nsIContent* const content = nsIContent::FromNode(iter.GetCurrentNode()); - if (!content || !content->IsSelectable()) { - continue; + if (aOutContent) { + if (nsIContent* const outContent = + nsIContent::FromNode(shrunkenStart.GetContainer())) { + *aOutContent = do_AddRef(outContent).take(); } - // FIXME: GetFrameForNodeOffset() may return unselectable frame. It should - // have an option to stop digging it if it reaches unselectable child. - uint32_t outOffset = 0; - nsIFrame* const firstFrame = SelectionMovementUtils::GetFrameForNodeOffset( - content, 0, CaretAssociationHint::After, &outOffset); - if (NS_WARN_IF(!firstFrame)) { - continue; - } - MOZ_DIAGNOSTIC_ASSERT(firstFrame->GetContent()); - *aOutOffsetInFrameContent = static_cast<int32_t>(outOffset); - if (aOutContent) { - *aOutContent = do_AddRef(content).take(); - } - if (aOutOffsetInContent) { - *aOutOffsetInContent = 0; + } + if (aOutOffsetInContent) { + *aOutOffsetInContent = static_cast<int32_t>( + *shrunkenStart.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets)); + } + if (nsIContent* const child = shrunkenStart.GetChildAtOffset()) { + if (nsIFrame* const childFrame = child->GetPrimaryFrame()) { + const uint32_t offsetInFrameContent = 0u; + nsIFrame* const childFrameAtOffset = GetChildFrameContainingOffset( + childFrame, offsetInFrameContent, CaretAssociationHint::After); + MOZ_ASSERT(childFrameAtOffset); + // If the child is a non-selectable container which has padding or border, + // we want to put the caret before the start edge, but returning the frame + // makes the caller will get rect in its content. Therefore, do not + // return the position in the unselectable container frame. + if (!childFrameAtOffset->IsInlineFrame() || + childFrameAtOffset->IsSelfEmpty()) { + return {childFrameAtOffset, offsetInFrameContent}; + } } - return firstFrame; } - return nullptr; + nsIContent* const container = + nsIContent::FromNode(shrunkenStart.GetContainer()); + if (MOZ_UNLIKELY(!container)) { + return {}; + } + nsIFrame* const frame = container->GetPrimaryFrame(); + if (MOZ_UNLIKELY(!frame)) { + return {}; + } + MOZ_ASSERT(frame->IsSelectable()); + const uint32_t offsetInFrameContent = + *shrunkenStart.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets); + nsIFrame* const frameAtOffset = GetChildFrameContainingOffset( + frame, offsetInFrameContent, CaretAssociationHint::After); + MOZ_ASSERT(frameAtOffset); + return {frameAtOffset, offsetInFrameContent}; } -nsIFrame* AccessibleCaretManager::GetFrameForRangeEnd( - nsRange& aRange, int32_t* aOutOffsetInFrameContent, - nsIContent** aOutContent /* = nullptr */, +FrameAndOffset +AccessibleCaretManager::GetLastVisibleLeafFrameOrUnselectableChildFrame( + nsRange& aRange, nsIContent** aOutContent /* = nullptr */, int32_t* aOutOffsetInContent /* = nullptr */) const { if (!mPresShell) { - return nullptr; + return {}; } MOZ_ASSERT(GetCaretMode() == CaretMode::Selection); - MOZ_ASSERT(aOutOffsetInFrameContent, - "aOutOffsetInFrameContent shouldn't be nullptr!"); - - nsIContent* const endContent = - nsIContent::FromNodeOrNull(aRange.GetEndContainer()); - if (endContent && endContent->IsSelectable()) { - // FIXME: GetFrameForNodeOffset() may return unselectable frame. It should - // have an option to stop digging it if it reaches unselectable child. - uint32_t outOffset = 0; - nsIFrame* const endFrame = SelectionMovementUtils::GetFrameForNodeOffset( - endContent, aRange.EndOffset(), CaretAssociationHint::Before, - &outOffset); - if (endFrame) { - MOZ_DIAGNOSTIC_ASSERT(endFrame->GetContent()); - *aOutOffsetInFrameContent = static_cast<int32_t>(outOffset); - if (aOutContent) { - *aOutContent = do_AddRef(endContent).take(); - } - if (aOutOffsetInContent) { - *aOutOffsetInContent = static_cast<int32_t>(aRange.EndOffset()); - } - return endFrame; - } + + // FYI: aRange may be collapsed if `Selection` has multiple ranges. + if (MOZ_UNLIKELY(aRange.Collapsed())) { + return {}; } - UnsafePostContentIterator iter; - if (NS_WARN_IF(NS_FAILED(iter.Init(&aRange)))) { - return nullptr; + const RawRangeBoundary& shrunkenEnd = + SelectionMovementUtils::GetLastVisiblePointAtLeaf(aRange); + if (MOZ_UNLIKELY(!shrunkenEnd.IsSet())) { + return {}; } - iter.Last(); - for (; !iter.IsDone(); iter.Prev()) { - nsIContent* const content = nsIContent::FromNode(iter.GetCurrentNode()); - if (!content || !content->IsSelectable()) { - continue; - } - // FIXME: GetFrameForNodeOffset() may return unselectable frame. It should - // have an option to stop digging it if it reaches unselectable child. - uint32_t outOffset = 0; - nsIFrame* const lastFrame = SelectionMovementUtils::GetFrameForNodeOffset( - content, content->Length(), CaretAssociationHint::Before, &outOffset); - if (NS_WARN_IF(!lastFrame)) { - continue; - } - MOZ_DIAGNOSTIC_ASSERT(lastFrame->GetContent()); - *aOutOffsetInFrameContent = static_cast<int32_t>(outOffset); - if (aOutContent) { - *aOutContent = do_AddRef(content).take(); + if (aOutContent) { + if (nsIContent* const outContent = + nsIContent::FromNode(shrunkenEnd.GetContainer())) { + *aOutContent = do_AddRef(outContent).take(); } - if (aOutOffsetInContent) { - *aOutOffsetInContent = static_cast<int32_t>(content->Length()); + } + if (aOutOffsetInContent) { + *aOutOffsetInContent = static_cast<int32_t>( + *shrunkenEnd.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets)); + } + if (nsIContent* const previousSiblingOfChildAtOffset = shrunkenEnd.Ref()) { + if (nsIFrame* const childFrame = + previousSiblingOfChildAtOffset->GetPrimaryFrame()) { + const uint32_t offsetInChildFrameContent = + previousSiblingOfChildAtOffset->Length(); + nsIFrame* const childFrameAtOffset = GetChildFrameContainingOffset( + childFrame, offsetInChildFrameContent, CaretAssociationHint::Before); + MOZ_ASSERT(childFrameAtOffset); + // If the child is a non-selectable inline container which has padding or + // border, we want to put the caret after the end edge, but returning the + // frame makes the caller will get rect in its content. Therefore, do not + // return the position in the unselectable container frame. + if (!childFrameAtOffset->IsInlineFrame() || + childFrameAtOffset->IsSelfEmpty()) { + return {childFrameAtOffset, offsetInChildFrameContent}; + } } - return lastFrame; } - return nullptr; + nsIContent* const container = + nsIContent::FromNode(shrunkenEnd.GetContainer()); + if (MOZ_UNLIKELY(!container)) { + return {}; + } + nsIFrame* const frame = container->GetPrimaryFrame(); + if (MOZ_UNLIKELY(!frame)) { + return {}; + } + MOZ_ASSERT(frame->IsSelectable()); + const uint32_t offsetInFrameContent = + *shrunkenEnd.Offset(RawRangeBoundary::OffsetFilter::kValidOffsets); + nsIFrame* const frameAtOffset = GetChildFrameContainingOffset( + frame, offsetInFrameContent, CaretAssociationHint::Before); + MOZ_ASSERT(frameAtOffset); + return {frameAtOffset, offsetInFrameContent}; } bool AccessibleCaretManager::RestrictCaretDraggingOffsets( @@ -1181,29 +1212,29 @@ bool AccessibleCaretManager::RestrictCaretDraggingOffsets( nsDirection dir = mActiveCaret == mCarets.GetFirst() ? eDirPrevious : eDirNext; - int32_t offsetInFrameContent = 0; nsCOMPtr<nsIContent> content; int32_t offsetInContent = 0; - nsIFrame* frame = - dir == eDirNext - ? GetFrameForRangeStart(*GetSelection()->GetFirstRange(), - &offsetInFrameContent, - getter_AddRefs(content), &offsetInContent) - : GetFrameForRangeEnd(*GetSelection()->GetLastRange(), - &offsetInFrameContent, getter_AddRefs(content), - &offsetInContent); - if (!frame) { + const FrameAndOffset frameAndOffset = + dir == eDirNext ? GetFirstVisibleLeafFrameOrUnselectableChildFrame( + *GetSelection()->GetFirstRange(), + getter_AddRefs(content), &offsetInContent) + : GetLastVisibleLeafFrameOrUnselectableChildFrame( + *GetSelection()->GetLastRange(), + getter_AddRefs(content), &offsetInContent); + if (!frameAndOffset.mFrame) { return false; } // Compare the active caret's new position (aOffsets) to the inactive caret's // position. - NS_ASSERTION(offsetInFrameContent >= 0, - "offsetInFrameContent should not be negative"); + NS_ASSERTION(static_cast<int32_t>(frameAndOffset.mOffsetInFrameContent) >= 0, + "mOffsetInFrameContent should not be negative when casting to " + "signed integer"); const Maybe<int32_t> cmpToInactiveCaretPos = nsContentUtils::ComparePoints_AllowNegativeOffsets( - aOffsets.content, aOffsets.StartOffset(), frame->GetContent(), - offsetInFrameContent); + aOffsets.content, aOffsets.StartOffset(), + frameAndOffset.GetFrameContent(), + static_cast<int32_t>(frameAndOffset.mOffsetInFrameContent)); if (NS_WARN_IF(!cmpToInactiveCaretPos)) { // Potentially handle this properly when Selection across Shadow DOM // boundary is implemented @@ -1214,9 +1245,10 @@ bool AccessibleCaretManager::RestrictCaretDraggingOffsets( // Move one character (in the direction of dir) from the inactive caret's // position. This is the limit for the active caret's new position. PeekOffsetStruct limit( - eSelectCluster, dir, offsetInFrameContent, nsPoint(0, 0), + eSelectCluster, dir, + static_cast<int32_t>(frameAndOffset.mOffsetInFrameContent), nsPoint(0, 0), {PeekOffsetOption::JumpLines, PeekOffsetOption::StopAtScroller}); - nsresult rv = frame->PeekOffset(&limit); + nsresult rv = frameAndOffset.mFrame->PeekOffset(&limit); if (NS_FAILED(rv)) { limit.mResultContent = content; limit.mContentOffset = offsetInContent; diff --git a/layout/base/AccessibleCaretManager.h b/layout/base/AccessibleCaretManager.h @@ -27,6 +27,7 @@ struct nsPoint; namespace mozilla { class PresShell; +struct FrameAndOffset; // defined in SelectionMovementUtils.h namespace dom { class Element; class Selection; @@ -222,17 +223,13 @@ class AccessibleCaretManager { void SetSelectionDirection(nsDirection aDir) const; /** - * Return a frame and offset in aOutContent where the meaningful start of - * aRange. E.g., if aRange starts with non-selectable elements, this returns - * the first selectable content's frame and its start offset. + * Return a frame and offset where to put the first accessible caret in the + * selection mode. The result may be a non-selectable frame which is for a + * child of a selectable content. If aOutContent is set and its pointee is + * different from the frame content of the result, it means that the result is + * the child at aOutContent and aOutOffsetInContent. * * @param aRange The range, typically the first range of `Selection`. - * @param aOutOffsetInFrameContent - * Must not be nullptr. If the result is not nullptr, this - * will be set to the offset in result->GetContent(). - * NOTE: {result->GetContent(), *aOutOffsetInFrameContent} - * means that it's the start boundary of visible/meaningful - * selection start boundary at a leaf node like a `Text`. * @param aOutContent [optional] If set, this will be set to the first * selectable container in aRange. It's typically a * container element. @@ -242,26 +239,30 @@ class AccessibleCaretManager { * NOTE: {*aOutContent, *aOutOffsetInContent} means that * it's the start boundary of actual selectable range at a * container element. - * @return The first meaningful frame whose content is selected by - * aRange. Typically, a text frame or a image frame. + * @return mFrame is the first meaningful frame whose content is + * selected by aRange. Typically, a text frame or a image + * frame. Or a container frame which is not selectable but + * its parent is selectable. + * mOffsetInFrameContent is the offset in + * mFrame->GetContent(). + * I.e, if mFrame->GetContent() is not a void element, + * {mFrame->GetContent(), mOffsetInFrameContent} means that + * it's the start boundary of visible/meaningful selection + * start boundary at a leaf node like a `Text` or position + * at the first non-selectable element in selectable node. */ - nsIFrame* GetFrameForRangeStart(nsRange& aRange, - int32_t* aOutOffsetInFrameContent, - nsIContent** aOutContent = nullptr, - int32_t* aOutOffsetInContent = nullptr) const; + FrameAndOffset GetFirstVisibleLeafFrameOrUnselectableChildFrame( + nsRange& aRange, nsIContent** aOutContent = nullptr, + int32_t* aOutOffsetInContent = nullptr) const; /** - * Return a frame and offset in aOutContent where the meaningful end of - * aRange. E.g., if aRange ends with non-selectable elements, this returns - * the last selectable content's frame and its end offset. + * Return a frame and offset where to put the last accessible caret in the + * selection mode. The result may be a non-selectable frame which is for a + * child of a selectable content. If aOutContent is set and its pointee is + * different from the frame content of the result, it means that the result is + * the previous sibling of a child at aOutContent and aOutOffsetInContent. * * @param aRange The range, typically the last range of `Selection`. - * @param aOutOffsetInFrameContent - * Must not be nullptr. If the result is not nullptr, this - * will be set to the offset in result->GetContent(). - * NOTE: {result->GetContent(), *aOutOffsetInFrameContent} - * means that it's the end boundary of visible/meaningful - * selection end boundary at a leaf node like a `Text`. * @param aOutContent [optional] If set, this will be set to the last * selectable container in aRange. It's typically a * container element. @@ -271,13 +272,21 @@ class AccessibleCaretManager { * NOTE: {*aOutContent, *aOutOffsetInContent} means that * it's the end boundary of actual selectable range at a * container element. - * @return The last meaningful frame whose content is selected by - * aRange. Typically, a text frame or a image frame. + * @return mFrame is the last meaningful frame whose content is + * selected by aRange. Typically, a text frame or a image + * frame. Or a container frame which is not selectable but + * its parent is selectable. + * mOffsetInFrameContent is the offset in + * mFrame->GetContent(). + * I.e, if mFrame->GetContent() is not a void element, + * {mFrame->GetContent(), mOffsetInFrameContent} means that + * it's the end boundary of visible/meaningful selection + * end boundary at a leaf node like a `Text` or position at + * the last non-selectable element in selectable node. */ - nsIFrame* GetFrameForRangeEnd(nsRange& aRange, - int32_t* aOutOffsetInFrameContent, - nsIContent** aOutContent = nullptr, - int32_t* aOutOffsetInContent = nullptr) const; + FrameAndOffset GetLastVisibleLeafFrameOrUnselectableChildFrame( + nsRange& aRange, nsIContent** aOutContent = nullptr, + int32_t* aOutOffsetInContent = nullptr) const; MOZ_CAN_RUN_SCRIPT nsresult DragCaretInternal(const nsPoint& aPoint); nsPoint AdjustDragBoundary(const nsPoint& aPoint) const; diff --git a/layout/generic/SelectionMovementUtils.cpp b/layout/generic/SelectionMovementUtils.cpp @@ -981,13 +981,13 @@ PrimaryFrameData SelectionMovementUtils::GetPrimaryOrCaretFrameForNodeOffset( nullptr, aContent, aOffset, aHint, aCaretBidiLevel, aContent && aContent->IsEditable() ? ForceEditableRegion::Yes : ForceEditableRegion::No); - return {result.mFrame, result.mOffsetInFrameContent, result.mHint}; + return {{result.mFrame, result.mOffsetInFrameContent}, result.mHint}; } uint32_t offset = 0; nsIFrame* theFrame = SelectionMovementUtils::GetFrameForNodeOffset( aContent, aOffset, aHint, &offset); - return {theFrame, offset, aHint}; + return {{theFrame, offset}, aHint}; } } // namespace mozilla diff --git a/layout/generic/SelectionMovementUtils.h b/layout/generic/SelectionMovementUtils.h @@ -25,12 +25,17 @@ namespace intl { class BidiEmbeddingLevel; } -struct MOZ_STACK_CLASS PrimaryFrameData { - // The frame which should be used to layout the caret. +struct MOZ_STACK_CLASS FrameAndOffset { + [[nodiscard]] nsIContent* GetFrameContent() const { + return mFrame ? mFrame->GetContent() : nullptr; + } + nsIFrame* mFrame = nullptr; - // The offset in content of mFrame. This is valid only when mFrame is not - // nullptr. + // The offset in mFrame->GetContent(). uint32_t mOffsetInFrameContent = 0; +}; + +struct MOZ_STACK_CLASS PrimaryFrameData : public FrameAndOffset { // Whether the caret should be put before or after the point. This is valid // only when mFrame is not nullptr. CaretAssociationHint mHint{0}; // Before diff --git a/testing/web-platform/meta/selection/shadow-dom/cross-shadow-boundary-1.html.ini b/testing/web-platform/meta/selection/shadow-dom/cross-shadow-boundary-1.html.ini @@ -0,0 +1,2 @@ +[cross-shadow-boundary-1.html] + expected: FAIL diff --git a/testing/web-platform/mozilla/meta/selection/select-text-and-invisible-br.html.ini b/testing/web-platform/mozilla/meta/selection/select-text-and-invisible-br.html.ini @@ -0,0 +1,2 @@ +[select-text-and-invisible-br.html] + prefs: [layout.accessiblecaret.enabled:true] diff --git a/testing/web-platform/mozilla/meta/selection/select-unselectable-img.html.ini b/testing/web-platform/mozilla/meta/selection/select-unselectable-img.html.ini @@ -0,0 +1,2 @@ +[select-unselectable-img.html] + prefs: [layout.accessiblecaret.enabled:true] diff --git a/testing/web-platform/mozilla/meta/selection/select-unselectable-span-1.html.ini b/testing/web-platform/mozilla/meta/selection/select-unselectable-span-1.html.ini @@ -0,0 +1,3 @@ +[select-unselectable-span-1.html] + prefs: [layout.accessiblecaret.enabled:true] + expected: FAIL diff --git a/testing/web-platform/mozilla/meta/selection/select-unselectable-span-2.html.ini b/testing/web-platform/mozilla/meta/selection/select-unselectable-span-2.html.ini @@ -0,0 +1,2 @@ +[select-unselectable-span-2.html] + prefs: [layout.accessiblecaret.enabled:true] diff --git a/testing/web-platform/mozilla/meta/selection/select-unselectable-text.html.ini b/testing/web-platform/mozilla/meta/selection/select-unselectable-text.html.ini @@ -0,0 +1,2 @@ +[select-unselectable-text.html] + prefs: [layout.accessiblecaret.enabled:true] diff --git a/testing/web-platform/mozilla/tests/selection/select-text-and-invisible-br.html b/testing/web-platform/mozilla/tests/selection/select-text-and-invisible-br.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<link rel="match" href="select-text-ref.html"> +</head> +<body> + <div>ABC<br></div> + <script> + getSelection().selectAllChildren(document.querySelector("div")); + </script> +</body> +</html> diff --git a/testing/web-platform/mozilla/tests/selection/select-text-ref.html b/testing/web-platform/mozilla/tests/selection/select-text-ref.html @@ -0,0 +1,12 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + <div>ABC</div> + <script> + getSelection().selectAllChildren(document.querySelector("div")); + </script> +</body> +</html> diff --git a/testing/web-platform/mozilla/tests/selection/select-unselectable-img-ref.html b/testing/web-platform/mozilla/tests/selection/select-unselectable-img-ref.html @@ -0,0 +1,21 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<style> +img::selection { + background-color: transparent; + color: currentColor; +} +</style> +</head> +<body> + <div><img + style="width:100px;height:100px" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAE0lEQVR4nGNk+M+ADJgYGCjiAwBPkgEJOTC6CgAAAABJRU5ErkJggg==" + ></div> + <script> + getSelection().selectAllChildren(document.querySelector("div")); + </script> +</body> +</html> diff --git a/testing/web-platform/mozilla/tests/selection/select-unselectable-img.html b/testing/web-platform/mozilla/tests/selection/select-unselectable-img.html @@ -0,0 +1,16 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<link rel="match" href="select-unselectable-img-ref.html"> +</head> +<body> + <div><img + style="user-select:none;width:100px;height:100px" + src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAIAAAACDbGyAAAAE0lEQVR4nGNk+M+ADJgYGCjiAwBPkgEJOTC6CgAAAABJRU5ErkJggg==" + ></div> + <script> + getSelection().selectAllChildren(document.querySelector("div")); + </script> +</body> +</html> diff --git a/testing/web-platform/mozilla/tests/selection/select-unselectable-span-1-ref.html b/testing/web-platform/mozilla/tests/selection/select-unselectable-span-1-ref.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +</head> +<body> + <!-- Accessible carets should be put around the unselectable <span> --> + <div><span style="user-select:none"><span style="padding:100px">ABC</span></span></div> + <script> + getSelection().selectAllChildren(document.querySelector("div")); + </script> +</body> +</html> diff --git a/testing/web-platform/mozilla/tests/selection/select-unselectable-span-1.html b/testing/web-platform/mozilla/tests/selection/select-unselectable-span-1.html @@ -0,0 +1,14 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<link rel="match" href="select-unselectable-span-1-ref.html"> +</head> +<body> + <!-- Accessible carets should be put around the <span> having big padding --> + <div><span style="user-select:none;padding:100px">ABC</span></span></div> + <script> + getSelection().selectAllChildren(document.querySelector("div")); + </script> +</body> +</html> diff --git a/testing/web-platform/mozilla/tests/selection/select-unselectable-span-2-ref.html b/testing/web-platform/mozilla/tests/selection/select-unselectable-span-2-ref.html @@ -0,0 +1,19 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<style> +::selection { + color: currentColor; + background-color: transparent; +} +</style> +</head> +<body> + <!-- Accessible carets should be put around "ABC" --> + <div><span style="padding:100px">ABC</span></div> + <script> + getSelection().selectAllChildren(document.querySelector("div")); + </script> +</body> +</html> diff --git a/testing/web-platform/mozilla/tests/selection/select-unselectable-span-2.html b/testing/web-platform/mozilla/tests/selection/select-unselectable-span-2.html @@ -0,0 +1,14 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<link rel="mismatch" href="select-unselectable-span-2-ref.html"> +</head> +<body> + <!-- Accessible carets should be put around the <span> having big padding --> + <div><span style="user-select:none;padding:100px">ABC</span></span></div> + <script> + getSelection().selectAllChildren(document.querySelector("div")); + </script> +</body> +</html> diff --git a/testing/web-platform/mozilla/tests/selection/select-unselectable-text-ref.html b/testing/web-platform/mozilla/tests/selection/select-unselectable-text-ref.html @@ -0,0 +1,18 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<style> +span::selection { + background-color: transparent; + color: currentColor; +} +</style> +</head> +<body> + <div><span>ABC</span></div> + <script> + getSelection().selectAllChildren(document.querySelector("div")); + </script> +</body> +</html> diff --git a/testing/web-platform/mozilla/tests/selection/select-unselectable-text.html b/testing/web-platform/mozilla/tests/selection/select-unselectable-text.html @@ -0,0 +1,13 @@ +<!doctype html> +<html> +<head> +<meta charset="utf-8"> +<link rel="match" href="select-unselectable-text-ref.html"> +</head> +<body> + <div><span style="user-select:none">ABC</span></div> + <script> + getSelection().selectAllChildren(document.querySelector("div")); + </script> +</body> +</html> diff --git a/testing/web-platform/tests/css/css-scroll-anchoring/contenteditable-near-cursor.tentative.html b/testing/web-platform/tests/css/css-scroll-anchoring/contenteditable-near-cursor.tentative.html @@ -27,7 +27,8 @@ async_test((t) => { let range = document.createRange(); range.setStart(three, 0); range.setEnd(three, 1); - document.getSelection().addRange(range); + getSelection().removeAllRanges(); + getSelection().addRange(range); document.scrollingElement.scrollBy(0, 2200);