Bug 1666739 - Add an optional opacity threshold for visibility hit-test. r=mconley,miko

This is a best-effort thing of course, but so is the rest of the
visibility threshold stuff in practice and this should be good enough.

Differential Revision: https://phabricator.services.mozilla.com/D98360
This commit is contained in:
Emilio Cobos Álvarez 2020-12-04 00:48:45 +00:00
Родитель 296459ef20
Коммит 75be5de2e1
12 изменённых файлов: 134 добавлений и 44 удалений

Просмотреть файл

@ -12918,8 +12918,8 @@ already_AddRefed<nsDOMCaretPosition> Document::CaretPositionFromPoint(
nsIFrame* ptFrame = nsLayoutUtils::GetFrameForPoint(
RelativeTo{rootFrame}, pt,
{FrameForPointOption::IgnorePaintSuppression,
FrameForPointOption::IgnoreCrossDoc});
{{FrameForPointOption::IgnorePaintSuppression,
FrameForPointOption::IgnoreCrossDoc}});
if (!ptFrame) {
return nullptr;
}

Просмотреть файл

@ -332,6 +332,7 @@ Element* DocumentOrShadowRoot::GetFullscreenElement() {
namespace {
using FrameForPointOption = nsLayoutUtils::FrameForPointOption;
using FrameForPointOptions = nsLayoutUtils::FrameForPointOptions;
// Whether only one node or multiple nodes is requested.
enum class Multiple {
@ -360,7 +361,7 @@ nsINode* CastTo<nsINode>(nsIContent* aContent) {
template <typename NodeOrElement>
static void QueryNodesFromRect(DocumentOrShadowRoot& aRoot, const nsRect& aRect,
EnumSet<FrameForPointOption> aOptions,
FrameForPointOptions aOptions,
FlushLayout aShouldFlushLayout,
Multiple aMultiple, ViewportType aViewportType,
nsTArray<RefPtr<NodeOrElement>>& aNodes) {
@ -390,8 +391,8 @@ static void QueryNodesFromRect(DocumentOrShadowRoot& aRoot, const nsRect& aRect,
return; // return null to premature XUL callers as a reminder to wait
}
aOptions += FrameForPointOption::IgnorePaintSuppression;
aOptions += FrameForPointOption::IgnoreCrossDoc;
aOptions.mBits += FrameForPointOption::IgnorePaintSuppression;
aOptions.mBits += FrameForPointOption::IgnoreCrossDoc;
AutoTArray<nsIFrame*, 8> frames;
nsLayoutUtils::GetFramesForArea({rootFrame, aViewportType}, aRect, frames,
@ -436,12 +437,12 @@ static void QueryNodesFromRect(DocumentOrShadowRoot& aRoot, const nsRect& aRect,
template <typename NodeOrElement>
static void QueryNodesFromPoint(DocumentOrShadowRoot& aRoot, float aX, float aY,
EnumSet<FrameForPointOption> aOptions,
FrameForPointOptions aOptions,
FlushLayout aShouldFlushLayout,
Multiple aMultiple, ViewportType aViewportType,
nsTArray<RefPtr<NodeOrElement>>& aNodes) {
// As per the spec, we return null if either coord is negative.
if (!aOptions.contains(FrameForPointOption::IgnoreRootScrollFrame) &&
if (!aOptions.mBits.contains(FrameForPointOption::IgnoreRootScrollFrame) &&
(aX < 0 || aY < 0)) {
return;
}
@ -499,6 +500,7 @@ void DocumentOrShadowRoot::NodesFromRect(float aX, float aY, float aTopSize,
float aLeftSize,
bool aIgnoreRootScrollFrame,
bool aFlushLayout, bool aOnlyVisible,
float aVisibleThreshold,
nsTArray<RefPtr<nsINode>>& aReturn) {
// Following the same behavior of elementFromPoint,
// we don't return anything if either coord is negative
@ -513,12 +515,13 @@ void DocumentOrShadowRoot::NodesFromRect(float aX, float aY, float aTopSize,
nsRect rect(x, y, w, h);
EnumSet<FrameForPointOption> options;
FrameForPointOptions options;
if (aIgnoreRootScrollFrame) {
options += FrameForPointOption::IgnoreRootScrollFrame;
options.mBits += FrameForPointOption::IgnoreRootScrollFrame;
}
if (aOnlyVisible) {
options += FrameForPointOption::OnlyVisible;
options.mBits += FrameForPointOption::OnlyVisible;
options.mVisibleThreshold = aVisibleThreshold;
}
auto flush = aFlushLayout ? FlushLayout::Yes : FlushLayout::No;

Просмотреть файл

@ -143,7 +143,8 @@ class DocumentOrShadowRoot {
void NodesFromRect(float aX, float aY, float aTopSize, float aRightSize,
float aBottomSize, float aLeftSize,
bool aIgnoreRootScrollFrame, bool aFlushLayout,
bool aOnlyVisible, nsTArray<RefPtr<nsINode>>&);
bool aOnlyVisible, float aVisibleThreshold,
nsTArray<RefPtr<nsINode>>&);
/**
* This gets fired when the element that an id refers to changes.

Просмотреть файл

@ -1238,15 +1238,23 @@ nsDOMWindowUtils::NodesFromRect(float aX, float aY, float aTopSize,
float aRightSize, float aBottomSize,
float aLeftSize, bool aIgnoreRootScrollFrame,
bool aFlushLayout, bool aOnlyVisible,
float aVisibleThreshold,
nsINodeList** aReturn) {
RefPtr<Document> doc = GetDocument();
NS_ENSURE_STATE(doc);
auto list = MakeRefPtr<nsSimpleContentList>(doc);
// The visible threshold was omitted or given a zero value (which makes no
// sense), so give a reasonable default.
if (aVisibleThreshold == 0.0f) {
aVisibleThreshold = 1.0f;
}
AutoTArray<RefPtr<nsINode>, 8> nodes;
doc->NodesFromRect(aX, aY, aTopSize, aRightSize, aBottomSize, aLeftSize,
aIgnoreRootScrollFrame, aFlushLayout, aOnlyVisible, nodes);
aIgnoreRootScrollFrame, aFlushLayout, aOnlyVisible,
aVisibleThreshold, nodes);
list->SetCapacity(nodes.Length());
for (auto& node : nodes) {
list->AppendElement(node->AsContent());

Просмотреть файл

@ -783,6 +783,11 @@ interface nsIDOMWindowUtils : nsISupports {
* @param aFlushLayout flushes layout if true. Otherwise, no flush occurs.
* @param aOnlyVisible Set to true if you only want nodes that pass a visibility
* hit test.
* @param aTransparencyThreshold Only has an effect if aOnlyVisible is true.
* Returns what amount of transparency is considered "opaque enough"
* to consider elements "not visible". The default is effectively "1"
* (so, only opaque elements will stop an element from being
* "visible").
*/
NodeList nodesFromRect(in float aX,
in float aY,
@ -792,7 +797,8 @@ interface nsIDOMWindowUtils : nsISupports {
in float aLeftSize,
in boolean aIgnoreRootScrollFrame,
in boolean aFlushLayout,
in boolean aOnlyVisible);
in boolean aOnlyVisible,
[optional] in float aTransparencyThreshold);
/**

Просмотреть файл

@ -13,8 +13,8 @@
let dwu = window.windowUtils;
function check(x, y, top, right, bottom, left, onlyVisible, list, aListIsComplete) {
let nodes = dwu.nodesFromRect(x, y, top, right, bottom, left, true, false, onlyVisible);
function check(x, y, top, right, bottom, left, onlyVisible, list, aListIsComplete = false, aOpacityThreshold = 1.0) {
let nodes = dwu.nodesFromRect(x, y, top, right, bottom, left, /* aIgnoreRootScrollFrame = */ true, /* aFlushLayout = */ true, onlyVisible, aOpacityThreshold);
if (!aListIsComplete) {
list.push(e.body);
@ -118,7 +118,23 @@
let [x, y] = getCenterFor(container);
check(x, y, 1, 1, 1, 1, false, [fg, bg, container]);
check(x, y, 1, 1, 1, 1, true, [fg], true);
const kListIsComplete = true;
check(x, y, 1, 1, 1, 1, true, [fg], kListIsComplete);
check(x, y, 1, 1, 1, 1, true, [fg], kListIsComplete, 0.5);
// Occluded with different opacity thresholds, with background colors and
// opacity.
fg.style.backgroundColor = "rgba(0, 255, 0, 0.5)";
check(x, y, 1, 1, 1, 1, true, [fg], kListIsComplete, 0.4);
check(x, y, 1, 1, 1, 1, true, [fg, bg], kListIsComplete, 0.6);
fg.style.backgroundColor = "";
fg.style.opacity = "0.5";
check(x, y, 1, 1, 1, 1, true, [fg], kListIsComplete, 0.4);
check(x, y, 1, 1, 1, 1, true, [fg, bg], kListIsComplete, 0.6);
}
done();

Просмотреть файл

@ -42,7 +42,7 @@ static already_AddRefed<dom::Element> ElementFromPoint(
}
nsIFrame* frame = nsLayoutUtils::GetFrameForPoint(
RelativeTo{rootFrame, ViewportType::Visual}, CSSPoint::ToAppUnits(aPoint),
{FrameForPointOption::IgnorePaintSuppression});
{{FrameForPointOption::IgnorePaintSuppression}});
while (frame && (!frame->GetContent() ||
frame->GetContent()->IsInNativeAnonymousSubtree())) {
frame = nsLayoutUtils::GetParentOrPlaceholderFor(frame);

Просмотреть файл

@ -2641,8 +2641,7 @@ struct AutoNestedPaintCount {
#endif
nsIFrame* nsLayoutUtils::GetFrameForPoint(
RelativeTo aRelativeTo, nsPoint aPt,
EnumSet<FrameForPointOption> aOptions) {
RelativeTo aRelativeTo, nsPoint aPt, const FrameForPointOptions& aOptions) {
AUTO_PROFILER_LABEL("nsLayoutUtils::GetFrameForPoint", LAYOUT);
nsresult rv;
@ -2653,9 +2652,10 @@ nsIFrame* nsLayoutUtils::GetFrameForPoint(
return outFrames.Length() ? outFrames.ElementAt(0) : nullptr;
}
nsresult nsLayoutUtils::GetFramesForArea(
RelativeTo aRelativeTo, const nsRect& aRect,
nsTArray<nsIFrame*>& aOutFrames, EnumSet<FrameForPointOption> aOptions) {
nsresult nsLayoutUtils::GetFramesForArea(RelativeTo aRelativeTo,
const nsRect& aRect,
nsTArray<nsIFrame*>& aOutFrames,
const FrameForPointOptions& aOptions) {
AUTO_PROFILER_LABEL("nsLayoutUtils::GetFramesForArea", LAYOUT);
nsIFrame* frame = const_cast<nsIFrame*>(aRelativeTo.mFrame);
@ -2665,10 +2665,10 @@ nsresult nsLayoutUtils::GetFramesForArea(
builder.BeginFrame();
nsDisplayList list;
if (aOptions.contains(FrameForPointOption::IgnorePaintSuppression)) {
if (aOptions.mBits.contains(FrameForPointOption::IgnorePaintSuppression)) {
builder.IgnorePaintSuppression();
}
if (aOptions.contains(FrameForPointOption::IgnoreRootScrollFrame)) {
if (aOptions.mBits.contains(FrameForPointOption::IgnoreRootScrollFrame)) {
nsIFrame* rootScrollFrame = frame->PresShell()->GetRootScrollFrame();
if (rootScrollFrame) {
builder.SetIgnoreScrollFrame(rootScrollFrame);
@ -2677,12 +2677,13 @@ nsresult nsLayoutUtils::GetFramesForArea(
if (aRelativeTo.mViewportType == ViewportType::Layout) {
builder.SetIsRelativeToLayoutViewport();
}
if (aOptions.contains(FrameForPointOption::IgnoreCrossDoc)) {
if (aOptions.mBits.contains(FrameForPointOption::IgnoreCrossDoc)) {
builder.SetDescendIntoSubdocuments(false);
}
builder.SetHitTestIsForVisibility(
aOptions.contains(FrameForPointOption::OnlyVisible));
if (aOptions.mBits.contains(FrameForPointOption::OnlyVisible)) {
builder.SetHitTestIsForVisibility(aOptions.mVisibleThreshold);
}
builder.EnterPresShell(frame);

Просмотреть файл

@ -747,16 +747,32 @@ class nsLayoutUtils {
OnlyVisible,
};
struct FrameForPointOptions {
using Bits = mozilla::EnumSet<FrameForPointOption>;
Bits mBits;
// If mBits contains OnlyVisible, what is the opacity threshold which we
// consider "opaque enough" to clobber stuff underneath.
float mVisibleThreshold;
FrameForPointOptions(Bits aBits, float aVisibleThreshold)
: mBits(aBits), mVisibleThreshold(aVisibleThreshold){};
MOZ_IMPLICIT FrameForPointOptions(Bits aBits)
: FrameForPointOptions(aBits, 1.0f) {}
FrameForPointOptions() : FrameForPointOptions(Bits()){};
};
/**
* Given aFrame, the root frame of a stacking context, find its descendant
* frame under the point aPt that receives a mouse event at that location,
* or nullptr if there is no such frame.
* @param aPt the point, relative to the frame origin, in either visual
* or layout coordinates depending on aRelativeTo.mViewportType
* @param aFlags some combination of FrameForPointOption.
*/
static nsIFrame* GetFrameForPoint(RelativeTo aRelativeTo, nsPoint aPt,
mozilla::EnumSet<FrameForPointOption> = {});
const FrameForPointOptions& = {});
/**
* Given aFrame, the root frame of a stacking context, find all descendant
@ -765,11 +781,10 @@ class nsLayoutUtils {
* @param aRect the rect, relative to the frame origin, in either visual
* or layout coordinates depending on aRelativeTo.mViewportType
* @param aOutFrames an array to add all the frames found
* @param aFlags some combination of FrameForPointOption.
*/
static nsresult GetFramesForArea(RelativeTo aRelativeTo, const nsRect& aRect,
nsTArray<nsIFrame*>& aOutFrames,
mozilla::EnumSet<FrameForPointOption> = {});
const FrameForPointOptions& = {});
/**
* Transform aRect relative to aFrame up to the coordinate system of

Просмотреть файл

@ -608,7 +608,6 @@ nsDisplayListBuilder::nsDisplayListBuilder(nsIFrame* aReferenceFrame,
mForceLayerForScrollParent(false),
mAsyncPanZoomEnabled(nsLayoutUtils::AsyncPanZoomEnabled(aReferenceFrame)),
mBuildingInvisibleItems(false),
mHitTestIsForVisibility(false),
mIsBuilding(false),
mInInvalidSubtree(false),
mDisablePartialUpdates(false),
@ -2810,9 +2809,37 @@ void nsDisplayList::HitTest(nsDisplayListBuilder* aBuilder, const nsRect& aRect,
}
if (aBuilder->HitTestIsForVisibility()) {
if (aState->mHitFullyOpaqueItem ||
item->GetOpaqueRegion(aBuilder, &snap).Contains(aRect)) {
aState->mHitFullyOpaqueItem = true;
aState->mHitOccludingItem = [&] {
if (aState->mHitOccludingItem) {
// We already hit something before.
return true;
}
if (aState->mCurrentOpacity == 1.0f &&
item->GetOpaqueRegion(aBuilder, &snap).Contains(aRect)) {
// An opaque item always occludes everything. Note that we need to
// check wrapping opacity and such as well.
return true;
}
float threshold = aBuilder->VisibilityThreshold();
if (threshold == 1.0f) {
return false;
}
float itemOpacity = [&] {
switch (item->GetType()) {
case DisplayItemType::TYPE_OPACITY:
return static_cast<nsDisplayOpacity*>(item)->GetOpacity();
case DisplayItemType::TYPE_BACKGROUND_COLOR:
return static_cast<nsDisplayBackgroundColor*>(item)
->GetOpacity();
default:
// Be conservative and assume it won't occlude other items.
return 0.0f;
}
}();
return itemOpacity * aState->mCurrentOpacity >= threshold;
}();
if (aState->mHitOccludingItem) {
// We're exiting early, so pop the remaining items off the buffer.
aState->mItemBuffer.TruncateLength(itemBufferStart);
break;
@ -5777,6 +5804,9 @@ void nsDisplayOpacity::HitTest(nsDisplayListBuilder* aBuilder,
const nsRect& aRect,
nsDisplayItem::HitTestState* aState,
nsTArray<nsIFrame*>* aOutFrames) {
AutoRestore<float> opacity(aState->mCurrentOpacity);
aState->mCurrentOpacity *= mOpacity;
// TODO(emilio): special-casing zero is a bit arbitrary... Maybe we should
// only consider fully opaque items? Or make this configurable somehow?
if (aBuilder->HitTestIsForVisibility() && mOpacity == 0.0f) {

Просмотреть файл

@ -1769,10 +1769,15 @@ class nsDisplayListBuilder {
AnimatedGeometryRoot* AnimatedGeometryRootForASR(
const ActiveScrolledRoot* aASR);
bool HitTestIsForVisibility() const { return mHitTestIsForVisibility; }
bool HitTestIsForVisibility() const { return mVisibleThreshold.isSome(); }
void SetHitTestIsForVisibility(bool aHitTestIsForVisibility) {
mHitTestIsForVisibility = aHitTestIsForVisibility;
float VisibilityThreshold() const {
MOZ_DIAGNOSTIC_ASSERT(HitTestIsForVisibility());
return mVisibleThreshold.valueOr(1.0f);
}
void SetHitTestIsForVisibility(float aVisibleThreshold) {
mVisibleThreshold = mozilla::Some(aVisibleThreshold);
}
bool ShouldBuildAsyncZoomContainer() const {
@ -2054,7 +2059,6 @@ class nsDisplayListBuilder {
bool mForceLayerForScrollParent;
bool mAsyncPanZoomEnabled;
bool mBuildingInvisibleItems;
bool mHitTestIsForVisibility;
bool mIsBuilding;
bool mInInvalidSubtree;
bool mBuildCompositorHitTestInfo;
@ -2066,6 +2070,7 @@ class nsDisplayListBuilder {
bool mIsRelativeToLayoutViewport;
bool mUseOverlayScrollbars;
mozilla::Maybe<float> mVisibleThreshold;
nsRect mHitTestArea;
CompositorHitTestInfo mHitTestInfo;
};
@ -2607,10 +2612,13 @@ class nsDisplayItem : public nsDisplayItemBase {
// Handling transform items for preserve 3D frames.
bool mInPreserves3D = false;
// When hit-testing for visibility, we may hit a fully opaque item in a
// When hit-testing for visibility, we may hit an fully opaque item in a
// nested display list. We want to stop at that point, without looking
// further on other items.
bool mHitFullyOpaqueItem = false;
bool mHitOccludingItem = false;
float mCurrentOpacity = 1.0f;
AutoTArray<nsDisplayItem*, 100> mItemBuffer;
};
@ -4997,6 +5005,8 @@ class nsDisplayBackgroundColor : public nsPaintedDisplayItem {
bool CanApplyOpacity() const override;
float GetOpacity() const { return mColor.a; }
nsRect GetBounds(nsDisplayListBuilder* aBuilder, bool* aSnap) const override {
*aSnap = true;
return mBackgroundRect;

Просмотреть файл

@ -1174,9 +1174,9 @@ bool nsTypeAheadFind::IsRangeRendered(nsRange* aRange) {
// Append visible frames to frames array.
nsLayoutUtils::GetFramesForArea(
RelativeTo{rootFrame}, r, frames,
{FrameForPointOption::IgnorePaintSuppression,
FrameForPointOption::IgnoreRootScrollFrame,
FrameForPointOption::OnlyVisible});
{{FrameForPointOption::IgnorePaintSuppression,
FrameForPointOption::IgnoreRootScrollFrame,
FrameForPointOption::OnlyVisible}});
// See if any of the frames contain the content. If they do, then the range
// is visible. We search for the content rather than the original frame,