/* 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>& aInitialRanges, nsPIDOMWindowInner* aWindow, ErrorResult& aRv) : mWindow(aWindow) { for (RefPtr range : aInitialRanges) { Add(*range, aRv); if (aRv.Failed()) { return; } } } already_AddRefed Highlight::Constructor( const GlobalObject& aGlobal, const Sequence>& aInitialRanges, ErrorResult& aRv) { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr window = do_QueryInterface(aGlobal.GetAsSupports()); if (!window) { aRv.ThrowUnknownError( "There is no window associated to " "this highlight object!"); return nullptr; } RefPtr 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& 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& 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& 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& 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> Highlight::RangesAtPoint( float aX, float aY, const Sequence>& aShadowRoots, Element* aElementAtPoint) const { if (!aElementAtPoint) { return {}; } AutoTArray, 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 // , 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 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 aGivenProto) { return Highlight_Binding::Wrap(aCx, this, aGivenProto); } } // namespace mozilla::dom