Files
Masayuki Nakano d3710534b7 Bug 2014622 - part 3: Fix the new WPT failure of shadow-dom/HighlightRegistry-highlightsFromPoint.html r=jjaschke
Fixing `ContentSubtreeIterator` bugs makes stop failing in some cases.
Then, `AbstractRange::CollectClientRectsAndText()` is changed and
`Highlight::RangesAtPoint()` starts appending ranges in a shadow
which is covered by an assigned node of a `<slot>` in the shadow since
selecting a `<slot>` succeeds the hit-testing in its assigned nodes.

The failing test is:
https://searchfox.org/firefox-main/rev/eec2f8dc61cc9f851fe5303cc05fc015d3128e6f/testing/web-platform/tests/shadow-dom/HighlightRegistry-highlightsFromPoint.html#170-174,176-181,185-187,197-201

Differential Revision: https://phabricator.services.mozilla.com/D307951
2026-07-05 23:40:42 +00:00

286 lines
10 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* 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 "Highlight.h"
#include "AbstractRange.h"
#include "Document.h"
#include "HighlightRegistry.h"
#include "Selection.h"
#include "mozilla/AlreadyAddRefed.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/RefPtr.h"
#include "mozilla/StaticAnalysisFunctions.h"
#include "mozilla/dom/HighlightBinding.h"
#include "nsFrameSelection.h"
#include "nsIContentInlines.h"
#include "nsLayoutUtils.h"
#include "nsPIDOMWindow.h"
#include "nsPresContext.h"
namespace mozilla::dom {
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(Highlight, mRanges, mWindow)
NS_IMPL_CYCLE_COLLECTING_ADDREF(Highlight)
NS_IMPL_CYCLE_COLLECTING_RELEASE(Highlight)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Highlight)
NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
Highlight::Highlight(
const Sequence<OwningNonNull<AbstractRange>>& aInitialRanges,
nsPIDOMWindowInner* aWindow, ErrorResult& aRv)
: mWindow(aWindow) {
for (RefPtr<AbstractRange> range : aInitialRanges) {
Add(*range, aRv);
if (aRv.Failed()) {
return;
}
}
}
already_AddRefed<Highlight> Highlight::Constructor(
const GlobalObject& aGlobal,
const Sequence<OwningNonNull<AbstractRange>>& aInitialRanges,
ErrorResult& aRv) {
MOZ_ASSERT(NS_IsMainThread());
nsCOMPtr<nsPIDOMWindowInner> window =
do_QueryInterface(aGlobal.GetAsSupports());
if (!window) {
aRv.ThrowUnknownError(
"There is no window associated to "
"this highlight object!");
return nullptr;
}
RefPtr<Highlight> highlight = new Highlight(aInitialRanges, window, aRv);
return aRv.Failed() ? nullptr : highlight.forget();
}
void Highlight::AddToHighlightRegistry(HighlightRegistry& aHighlightRegistry,
nsAtom& aHighlightName) {
mHighlightRegistries.LookupOrInsert(&aHighlightRegistry)
.Insert(&aHighlightName);
}
void Highlight::RemoveFromHighlightRegistry(
HighlightRegistry& aHighlightRegistry, nsAtom& aHighlightName) {
if (auto entry = mHighlightRegistries.Lookup(&aHighlightRegistry)) {
auto& highlightNames = entry.Data();
highlightNames.Remove(&aHighlightName);
if (highlightNames.IsEmpty()) {
entry.Remove();
}
}
}
void Highlight::Repaint() {
for (const RefPtr<HighlightRegistry>& registry :
mHighlightRegistries.Keys()) {
registry->RepaintHighlightSelection(*this);
}
}
void Highlight::SetPriority(int32_t aPriority) {
if (mPriority == aPriority) {
return;
}
mPriority = aPriority;
Repaint();
}
void Highlight::SetType(HighlightType aHighlightType) {
if (mHighlightType == aHighlightType) {
return;
}
mHighlightType = aHighlightType;
Repaint();
}
Highlight* Highlight::Add(AbstractRange& aRange, ErrorResult& aRv) {
// Manually check if the range `aKey` is already present in this highlight,
// because `SetlikeHelpers::Add()` doesn't indicate this.
// To keep the setlike and the mirrored array in sync, the range must not
// be added to `mRanges` if it was already present.
// `SetlikeHelpers::Has()` is much faster in checking this than
// `nsTArray<>::Contains()`.
if (Highlight_Binding::SetlikeHelpers::Has(this, aRange, aRv) ||
aRv.Failed()) {
return this;
}
Highlight_Binding::SetlikeHelpers::Add(this, aRange, aRv);
if (aRv.Failed()) {
return this;
}
MOZ_ASSERT(!mRanges.Contains(&aRange),
"setlike and DOM mirror are not in sync");
mRanges.AppendElement(&aRange);
AutoFrameSelectionBatcher selectionBatcher(__FUNCTION__,
mHighlightRegistries.Count());
for (const RefPtr<HighlightRegistry>& registry :
mHighlightRegistries.Keys()) {
auto frameSelection = registry->GetFrameSelection();
selectionBatcher.AddFrameSelection(frameSelection);
// since this is run in a context guarded by a selection batcher,
// no strong reference is needed to keep `registry` alive.
MOZ_KnownLive(registry)->MaybeAddRangeToHighlightSelection(aRange, *this);
if (aRv.Failed()) {
return this;
}
}
return this;
}
void Highlight::Clear(ErrorResult& aRv) {
Highlight_Binding::SetlikeHelpers::Clear(this, aRv);
if (!aRv.Failed()) {
mRanges.Clear();
AutoFrameSelectionBatcher selectionBatcher(__FUNCTION__,
mHighlightRegistries.Count());
for (const RefPtr<HighlightRegistry>& registry :
mHighlightRegistries.Keys()) {
auto frameSelection = registry->GetFrameSelection();
selectionBatcher.AddFrameSelection(frameSelection);
// since this is run in a context guarded by a selection batcher,
// no strong reference is needed to keep `registry` alive.
MOZ_KnownLive(registry)->RemoveHighlightSelection(*this);
}
}
}
bool Highlight::Delete(AbstractRange& aRange, ErrorResult& aRv) {
if (Highlight_Binding::SetlikeHelpers::Delete(this, aRange, aRv)) {
mRanges.RemoveElement(&aRange);
AutoFrameSelectionBatcher selectionBatcher(__FUNCTION__,
mHighlightRegistries.Count());
for (const RefPtr<HighlightRegistry>& registry :
mHighlightRegistries.Keys()) {
auto frameSelection = registry->GetFrameSelection();
selectionBatcher.AddFrameSelection(frameSelection);
// since this is run in a context guarded by a selection batcher,
// no strong reference is needed to keep `registry` alive.
MOZ_KnownLive(registry)->MaybeRemoveRangeFromHighlightSelection(aRange,
*this);
}
return true;
}
return false;
}
// Callback that sets mHit if any rect covers (xAppUnits, yAppUnits).
struct PointHitCallback : public RectCallback {
const nscoord mX, mY;
bool mHit = false;
PointHitCallback(nscoord aX, nscoord aY) : mX(aX), mY(aY) {}
void AddRect(const nsRect& aRect) override {
if (!mHit) {
mHit = aRect.Contains(mX, mY);
}
}
};
// https://drafts.csswg.org/css-highlight-api-1/#dom-highlightregistry-highlightsfrompoint
nsTArray<RefPtr<AbstractRange>> Highlight::RangesAtPoint(
float aX, float aY,
const Sequence<OwningNonNull<mozilla::dom::ShadowRoot>>& aShadowRoots,
Element* aElementAtPoint) const {
if (!aElementAtPoint) {
return {};
}
AutoTArray<RefPtr<AbstractRange>, 4> rangesAtPoint;
// Convert once; all client rects from CollectClientRects() are in app units.
const nscoord xAppUnits = nsPresContext::CSSPixelsToAppUnits(aX);
const nscoord yAppUnits = nsPresContext::CSSPixelsToAppUnits(aY);
// 3.2. For each AbstractRange abstractRange in highlight:
ShadowRoot* const containingShadowAtPoint =
aElementAtPoint->GetContainingShadowForSelection();
ShadowRoot* const shadowRootCoveredByElementAtPoint = [&]() {
// If aElementAtPoint is an inclusive descendant of an assigned node of a
// <slot>, the range in an assigned node hides the shadow at the point.
// Then, we should not append ranges that appear in the shadow.
ShadowRoot* const closestShadowRootInFlattenedTree =
aElementAtPoint->GetClosestShadowRootInFlattenedTreeForSelection();
return containingShadowAtPoint != closestShadowRootInFlattenedTree
? closestShadowRootInFlattenedTree
: nullptr;
}();
for (const auto& range : mRanges) {
// 3.2.1. If abstractRange is an invalid StaticRange, then continue.
if (range->IsStaticRange() && !range->AsStaticRange()->IsValid()) {
continue;
}
if (!range->IsPositioned()) {
continue;
}
// The spec leaves hit-testing details out of scope. As an implementation
// choice we filter ranges by shadow tree membership relative to the point:
// - If the point is in shadow tree T (aPointShadowRoot), only include
// ranges whose closestCommonAncestorContainer is also in T. Highlights
// rooted in the light DOM dont paint over shadow DOM content, even if
// their client rects happen to cover the shadow hosts layout box.
// - If the point is in the light DOM, skip ranges whose
// closestCommonAncestorContainer is in a shadow tree not listed in
// options.shadowRoots.
const nsINode* const closestCommonAncestor =
range->GetClosestCommonInclusiveAncestor();
if (!closestCommonAncestor) {
continue;
}
const ShadowRoot* const containingShadow =
closestCommonAncestor->GetContainingShadow();
if (containingShadowAtPoint) {
if (containingShadow != containingShadowAtPoint) {
// The range is not selecting content in the shadow root.
continue;
}
} else if (closestCommonAncestor->IsInShadowTree() &&
!aShadowRoots.Contains(containingShadow)) {
// The range is in a shadow, but the shadow root is not listed in
// options.shadowRoots.
continue;
}
// The range in the shadow should not be appended when the element at the
// point is an assigned slot of a <slot> in the shadow since the element
// covers the shadow and is not contained by the shadow.
if (shadowRootCoveredByElementAtPoint &&
containingShadow == shadowRootCoveredByElementAtPoint) {
continue;
}
// 3.2.2. Let range be a new Range whose start/end nodes and offsets are
// set to abstractRanges.
// Omitted: getClientRects() is implemented directly on AbstractRange, so no
// temporary Range object is needed.
// 3.2.3. If the coordinates x,y fall inside at least one of the DOMRects
// returned by calling getClientRects() on range, then append
// abstractRange to result.ranges.
// Note: Layout was already flushed at the callsite.
PointHitCallback hitTest(xAppUnits, yAppUnits);
range->CollectClientRects(hitTest, /* aClampToEdge */ true);
if (hitTest.mHit) {
rangesAtPoint.AppendElement(range);
}
}
return std::move(rangesAtPoint);
}
JSObject* Highlight::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return Highlight_Binding::Wrap(aCx, this, aGivenProto);
}
} // namespace mozilla::dom