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
286 lines
10 KiB
C++
286 lines
10 KiB
C++
/* 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 don’t paint over shadow DOM content, even if
|
||
// their client rects happen to cover the shadow host’s 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 abstractRange’s.
|
||
// 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
|