зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1305957 part 5 - Add implementation of scroll anchor selection and invalidation. r=hiro,dbaron,dholbert
This commit implements candidate selection for a scroll frame using a frame tree traversal. It roughly tries to follow the algorithm given in the scroll anchoring draft specification, adapted to operate on the frame tree [1]. Some details, such as not selecting an anchor if the user hasn't scrolled are not currently in the specification but will be to match Blink's implementation. Once a scroll anchor has been selected, we maintain a bit on it and its ancestor frame's states. This is used in a later commit to detect changes to position during a reflow so the scroll frame can perform an adjustment. A scroll anchor will be invalidated when the user scrolls the frame or the scroll anchor is destroyed. Later commits will add logic to drive selection and invalidation appropriately. [1] https://drafts.csswg.org/css-scroll-anchoring/#anchor-node-selection Differential Revision: https://phabricator.services.mozilla.com/D13268 --HG-- extra : rebase_source : e05ba48662aafef5957322def33ddc5d93f3ca5a extra : source : bdb766faa8679386bf4e9740781262ea4bb36544
This commit is contained in:
Родитель
91f41f3a0a
Коммит
47ee617d1e
|
@ -1490,11 +1490,11 @@ bool nsLayoutUtils::IsAncestorFrameCrossDoc(const nsIFrame* aAncestorFrame,
|
|||
}
|
||||
|
||||
// static
|
||||
bool nsLayoutUtils::IsProperAncestorFrame(nsIFrame* aAncestorFrame,
|
||||
nsIFrame* aFrame,
|
||||
nsIFrame* aCommonAncestor) {
|
||||
bool nsLayoutUtils::IsProperAncestorFrame(const nsIFrame* aAncestorFrame,
|
||||
const nsIFrame* aFrame,
|
||||
const nsIFrame* aCommonAncestor) {
|
||||
if (aFrame == aAncestorFrame) return false;
|
||||
for (nsIFrame* f = aFrame; f != aCommonAncestor; f = f->GetParent()) {
|
||||
for (const nsIFrame* f = aFrame; f != aCommonAncestor; f = f->GetParent()) {
|
||||
if (f == aAncestorFrame) return true;
|
||||
}
|
||||
return aCommonAncestor == aAncestorFrame;
|
||||
|
|
|
@ -528,8 +528,9 @@ class nsLayoutUtils {
|
|||
* aAncestorFrame. If non-null, this can bound the search and speed up
|
||||
* the function
|
||||
*/
|
||||
static bool IsProperAncestorFrame(nsIFrame* aAncestorFrame, nsIFrame* aFrame,
|
||||
nsIFrame* aCommonAncestor = nullptr);
|
||||
static bool IsProperAncestorFrame(const nsIFrame* aAncestorFrame,
|
||||
const nsIFrame* aFrame,
|
||||
const nsIFrame* aCommonAncestor = nullptr);
|
||||
|
||||
/**
|
||||
* Like IsProperAncestorFrame, but looks across document boundaries.
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include "ScrollAnchorContainer.h"
|
||||
|
||||
#include "mozilla/StaticPrefs.h"
|
||||
#include "nsGfxScrollFrame.h"
|
||||
#include "nsLayoutUtils.h"
|
||||
|
||||
|
@ -16,7 +17,10 @@ namespace mozilla {
|
|||
namespace layout {
|
||||
|
||||
ScrollAnchorContainer::ScrollAnchorContainer(ScrollFrameHelper* aScrollFrame)
|
||||
: mScrollFrame(aScrollFrame) {}
|
||||
: mScrollFrame(aScrollFrame),
|
||||
mAnchorNode(nullptr),
|
||||
mLastAnchorPos(0, 0),
|
||||
mAnchorNodeIsDirty(true) {}
|
||||
|
||||
ScrollAnchorContainer::~ScrollAnchorContainer() {}
|
||||
|
||||
|
@ -40,5 +44,393 @@ nsIScrollableFrame* ScrollAnchorContainer::ScrollableFrame() const {
|
|||
return Frame()->GetScrollTargetFrame();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the appropriate frame flags for a frame that has become or is no longer
|
||||
* an anchor node.
|
||||
*/
|
||||
static void SetAnchorFlags(const nsIFrame* aScrolledFrame,
|
||||
nsIFrame* aAnchorNode, bool aInScrollAnchorChain) {
|
||||
nsIFrame* frame = aAnchorNode;
|
||||
while (frame && frame != aScrolledFrame) {
|
||||
MOZ_ASSERT(
|
||||
frame == aAnchorNode || !frame->IsScrollFrame(),
|
||||
"We shouldn't select an anchor node inside a nested scroll frame.");
|
||||
|
||||
frame->SetInScrollAnchorChain(aInScrollAnchorChain);
|
||||
frame = frame->GetParent();
|
||||
}
|
||||
MOZ_ASSERT(frame,
|
||||
"The anchor node should be a descendant of the scroll frame");
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the scrollable overflow rect [1] of aCandidate relative to
|
||||
* aScrollFrame with all transforms applied. The specification is also
|
||||
* ambiguous about what can be selected as a scroll anchor, which makes
|
||||
* the scroll anchoring bounding rect partially undefined [2]. This code
|
||||
* attempts to match the implementation in Blink.
|
||||
*
|
||||
* [1]
|
||||
* https://drafts.csswg.org/css-scroll-anchoring-1/#scroll-anchoring-bounding-rect
|
||||
* [2] https://github.com/w3c/csswg-drafts/issues/3478
|
||||
*/
|
||||
static nsRect FindScrollAnchoringBoundingRect(const nsIFrame* aScrollFrame,
|
||||
nsIFrame* aCandidate) {
|
||||
MOZ_ASSERT(nsLayoutUtils::IsProperAncestorFrame(aScrollFrame, aCandidate));
|
||||
if (aCandidate->GetContent()->IsText()) {
|
||||
nsRect bounding;
|
||||
for (nsIFrame* continuation = aCandidate->FirstContinuation(); continuation;
|
||||
continuation = continuation->GetNextContinuation()) {
|
||||
nsRect localRect =
|
||||
continuation->GetScrollableOverflowRectRelativeToSelf();
|
||||
nsRect transformed = nsLayoutUtils::TransformFrameRectToAncestor(
|
||||
continuation, localRect, aScrollFrame);
|
||||
bounding = bounding.Union(transformed);
|
||||
}
|
||||
return bounding;
|
||||
}
|
||||
|
||||
nsRect localRect = aCandidate->GetScrollableOverflowRectRelativeToSelf();
|
||||
nsRect transformed = nsLayoutUtils::TransformFrameRectToAncestor(
|
||||
aCandidate, localRect, aScrollFrame);
|
||||
return transformed;
|
||||
}
|
||||
|
||||
void ScrollAnchorContainer::SelectAnchor() {
|
||||
MOZ_ASSERT(mScrollFrame->mScrolledFrame);
|
||||
MOZ_ASSERT(mAnchorNodeIsDirty);
|
||||
|
||||
if (!StaticPrefs::layout_css_scroll_anchoring_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ANCHOR_LOG("Selecting anchor for %p with scroll-port [%d %d x %d %d].\n",
|
||||
this, mScrollFrame->mScrollPort.x, mScrollFrame->mScrollPort.y,
|
||||
mScrollFrame->mScrollPort.width, mScrollFrame->mScrollPort.height);
|
||||
|
||||
const nsStyleDisplay* disp = Frame()->StyleDisplay();
|
||||
|
||||
// Don't select a scroll anchor if the scroll frame has `overflow-anchor:
|
||||
// none`.
|
||||
bool overflowAnchor =
|
||||
disp->mOverflowAnchor == mozilla::StyleOverflowAnchor::Auto;
|
||||
|
||||
// Or if the scroll frame has not been scrolled from the logical origin. This
|
||||
// is not in the specification [1], but Blink does this.
|
||||
//
|
||||
// [1] https://github.com/w3c/csswg-drafts/issues/3319
|
||||
bool isScrolled = mScrollFrame->GetLogicalScrollPosition() != nsPoint();
|
||||
|
||||
// Or if there is perspective that could affect the scrollable overflow rect
|
||||
// for descendant frames. This is not in the specification as Blink doesn't
|
||||
// share this behavior with perspective [1].
|
||||
//
|
||||
// [1] https://github.com/w3c/csswg-drafts/issues/3322
|
||||
bool hasPerspective = Frame()->ChildrenHavePerspective();
|
||||
|
||||
// Select a new scroll anchor
|
||||
nsIFrame* oldAnchor = mAnchorNode;
|
||||
if (overflowAnchor && isScrolled && !hasPerspective) {
|
||||
ANCHOR_LOG("Beginning candidate selection.\n");
|
||||
mAnchorNode = FindAnchorIn(mScrollFrame->mScrolledFrame);
|
||||
} else {
|
||||
if (!overflowAnchor) {
|
||||
ANCHOR_LOG("Skipping candidate selection for `overflow-anchor: none`\n");
|
||||
}
|
||||
if (!isScrolled) {
|
||||
ANCHOR_LOG("Skipping candidate selection for not being scrolled\n");
|
||||
}
|
||||
if (hasPerspective) {
|
||||
ANCHOR_LOG(
|
||||
"Skipping candidate selection for scroll frame with perspective\n");
|
||||
}
|
||||
mAnchorNode = nullptr;
|
||||
}
|
||||
|
||||
// Update the anchor flags if needed
|
||||
if (oldAnchor != mAnchorNode) {
|
||||
ANCHOR_LOG("Anchor node has changed from (%p) to (%p).\n", oldAnchor,
|
||||
mAnchorNode);
|
||||
|
||||
// Unset all flags for the old scroll anchor
|
||||
if (oldAnchor) {
|
||||
SetAnchorFlags(mScrollFrame->mScrolledFrame, oldAnchor, false);
|
||||
}
|
||||
|
||||
// Set all flags for the new scroll anchor
|
||||
if (mAnchorNode) {
|
||||
// Anchor selection will never select a descendant of a different scroll
|
||||
// frame, so we can set flags without conflicting with other scroll
|
||||
// anchor containers.
|
||||
SetAnchorFlags(mScrollFrame->mScrolledFrame, mAnchorNode, true);
|
||||
}
|
||||
} else {
|
||||
ANCHOR_LOG("Anchor node has remained (%p).\n", mAnchorNode);
|
||||
}
|
||||
|
||||
// Calculate the position to use for scroll adjustments
|
||||
if (mAnchorNode) {
|
||||
mLastAnchorPos =
|
||||
FindScrollAnchoringBoundingRect(Frame(), mAnchorNode).TopLeft();
|
||||
ANCHOR_LOG("Using last anchor position = [%d, %d].\n", mLastAnchorPos.x,
|
||||
mLastAnchorPos.y);
|
||||
} else {
|
||||
mLastAnchorPos = nsPoint();
|
||||
}
|
||||
|
||||
mAnchorNodeIsDirty = false;
|
||||
}
|
||||
|
||||
void ScrollAnchorContainer::UserScrolled() { InvalidateAnchor(); }
|
||||
|
||||
void ScrollAnchorContainer::InvalidateAnchor() {
|
||||
if (!StaticPrefs::layout_css_scroll_anchoring_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ANCHOR_LOG("Invalidating scroll anchor %p for %p.\n", mAnchorNode, this);
|
||||
|
||||
if (mAnchorNode) {
|
||||
SetAnchorFlags(mScrollFrame->mScrolledFrame, mAnchorNode, false);
|
||||
}
|
||||
mAnchorNode = nullptr;
|
||||
mAnchorNodeIsDirty = true;
|
||||
mLastAnchorPos = nsPoint();
|
||||
}
|
||||
|
||||
void ScrollAnchorContainer::Destroy() {
|
||||
if (mAnchorNode) {
|
||||
SetAnchorFlags(mScrollFrame->mScrolledFrame, mAnchorNode, false);
|
||||
}
|
||||
mAnchorNode = nullptr;
|
||||
mAnchorNodeIsDirty = false;
|
||||
mLastAnchorPos = nsPoint();
|
||||
}
|
||||
|
||||
ScrollAnchorContainer::ExamineResult
|
||||
ScrollAnchorContainer::ExamineAnchorCandidate(nsIFrame* aFrame) const {
|
||||
#ifdef DEBUG_FRAME_DUMP
|
||||
nsCString tag = aFrame->ListTag();
|
||||
ANCHOR_LOG("\tVisiting frame=%s (%p).\n", tag.get(), aFrame);
|
||||
#else
|
||||
ANCHOR_LOG("\t\tVisiting frame=%p.\n", aFrame);
|
||||
#endif
|
||||
|
||||
// Check if the author has opted out of scroll anchoring for this frame
|
||||
// and its descendants.
|
||||
const nsStyleDisplay* disp = aFrame->StyleDisplay();
|
||||
if (disp->mOverflowAnchor == mozilla::StyleOverflowAnchor::None) {
|
||||
ANCHOR_LOG("\t\tExcluding `overflow-anchor: none`.\n");
|
||||
return ExamineResult::Exclude;
|
||||
}
|
||||
|
||||
// Sticky positioned elements can move with the scroll frame, making them
|
||||
// unsuitable scroll anchors. This isn't in the specification yet [1], but
|
||||
// matches Blink's implementation.
|
||||
//
|
||||
// [1] https://github.com/w3c/csswg-drafts/issues/3319
|
||||
if (aFrame->IsStickyPositioned()) {
|
||||
ANCHOR_LOG("\t\tExcluding `position: sticky`.\n");
|
||||
return ExamineResult::Exclude;
|
||||
}
|
||||
|
||||
// The frame for a <br> element has a non-zero area, but Blink treats them
|
||||
// as if they have no area, so exclude them specially.
|
||||
if (aFrame->IsBrFrame()) {
|
||||
ANCHOR_LOG("\t\tExcluding <br>.\n");
|
||||
return ExamineResult::Exclude;
|
||||
}
|
||||
|
||||
// Exclude frames that aren't accessible to content.
|
||||
bool isChrome =
|
||||
aFrame->GetContent() && aFrame->GetContent()->ChromeOnlyAccess();
|
||||
bool isPseudo = aFrame->Style()->IsPseudoElement();
|
||||
if (isChrome && !isPseudo) {
|
||||
ANCHOR_LOG("\t\tExcluding chrome only content.\n");
|
||||
return ExamineResult::Exclude;
|
||||
}
|
||||
|
||||
// See if this frame could have its own anchor node. We could check
|
||||
// IsScrollFrame(), but that would miss nsListControlFrame which is not a
|
||||
// scroll frame, but still inherits from nsHTMLScrollFrame.
|
||||
nsIScrollableFrame* scrollable = do_QueryFrame(aFrame);
|
||||
|
||||
// We don't allow scroll anchors to be selected inside of scrollable frames as
|
||||
// it's not clear how an anchor adjustment should apply to multiple scrollable
|
||||
// frames. Blink allows this to happen, but they're not sure why [1].
|
||||
//
|
||||
// We also don't allow scroll anchors to be selected inside of SVG as it uses
|
||||
// a different layout model than CSS, and the specification doesn't say it
|
||||
// should apply.
|
||||
//
|
||||
// [1] https://github.com/w3c/csswg-drafts/issues/3477
|
||||
bool canDescend = !scrollable && !aFrame->IsSVGOuterSVGFrame();
|
||||
|
||||
// Check what kind of frame this is
|
||||
bool isBlockOutside = aFrame->IsBlockOutside();
|
||||
bool isText = aFrame->GetContent()->IsText();
|
||||
bool isAnonBox = aFrame->Style()->IsAnonBox() && !isText;
|
||||
bool isInlineOutside = aFrame->IsInlineOutside() && !isText;
|
||||
bool isContinuation = !!aFrame->GetPrevContinuation();
|
||||
|
||||
// If the frame is anonymous or inline-outside, search its descendants for a
|
||||
// scroll anchor.
|
||||
if ((isAnonBox || isInlineOutside) && canDescend) {
|
||||
ANCHOR_LOG(
|
||||
"\t\tSearching descendants of anon or inline box (a=%d, i=%d).\n",
|
||||
isAnonBox, isInlineOutside);
|
||||
return ExamineResult::PassThrough;
|
||||
}
|
||||
|
||||
// If the frame is not block-outside or a text node then exclude it.
|
||||
if (!isBlockOutside && !isText) {
|
||||
ANCHOR_LOG("\t\tExcluding non block-outside or text node (b=%d, t=%d).\n",
|
||||
isBlockOutside, isText);
|
||||
return ExamineResult::Exclude;
|
||||
}
|
||||
|
||||
// Find the scroll anchoring bounding rect.
|
||||
nsRect rect = FindScrollAnchoringBoundingRect(Frame(), aFrame);
|
||||
ANCHOR_LOG("\t\trect = [%d %d x %d %d].\n", rect.x, rect.y, rect.width,
|
||||
rect.height);
|
||||
|
||||
// Check if this frame is visible in the scroll port. This will exclude rects
|
||||
// with zero sized area. The specification is ambiguous about this [1], but
|
||||
// this matches Blink's implementation.
|
||||
//
|
||||
// [1] https://github.com/w3c/csswg-drafts/issues/3483
|
||||
nsRect visibleRect;
|
||||
if (!visibleRect.IntersectRect(rect, mScrollFrame->mScrollPort)) {
|
||||
return ExamineResult::Exclude;
|
||||
}
|
||||
|
||||
// At this point, if canDescend is true, we should only have visible
|
||||
// non-anonymous frames that are either:
|
||||
// 1. block-outside
|
||||
// 2. text nodes
|
||||
//
|
||||
// It's not clear what the scroll anchoring bounding rect of elements that are
|
||||
// block-outside should be when they are fragmented. For text nodes that are
|
||||
// fragmented, it's specified that we need to consider the union of its line
|
||||
// boxes.
|
||||
//
|
||||
// So for text nodes we handle them by including the union of line boxes in
|
||||
// the bounding rect of the primary frame, and not selecting any
|
||||
// continuations.
|
||||
//
|
||||
// For block-outside elements we choose to consider the bounding rect of each
|
||||
// frame individually, allowing ourselves to descend into any frame, but only
|
||||
// selecting a frame if it's not a continuation.
|
||||
if (canDescend && isContinuation) {
|
||||
ANCHOR_LOG("\t\tSearching descendants of a continuation.\n");
|
||||
return ExamineResult::PassThrough;
|
||||
}
|
||||
|
||||
// If this frame is fully visible, then select it as the scroll anchor.
|
||||
if (visibleRect.IsEqualEdges(rect)) {
|
||||
ANCHOR_LOG("\t\tFully visible, taking.\n");
|
||||
return ExamineResult::Accept;
|
||||
}
|
||||
|
||||
// If we can't descend into this frame, then select it as the scroll anchor.
|
||||
if (!canDescend) {
|
||||
ANCHOR_LOG("\t\tIntersects a frame that we can't descend into, taking.\n");
|
||||
return ExamineResult::Accept;
|
||||
}
|
||||
|
||||
// It must be partially visible and we can descend into this frame. Examine
|
||||
// its children for a better scroll anchor or fall back to this one.
|
||||
ANCHOR_LOG("\t\tIntersects valid candidate, checking descendants.\n");
|
||||
return ExamineResult::Traverse;
|
||||
}
|
||||
|
||||
nsIFrame* ScrollAnchorContainer::FindAnchorIn(nsIFrame* aFrame) const {
|
||||
// Visit the child lists of this frame
|
||||
for (nsIFrame::ChildListIterator lists(aFrame); !lists.IsDone();
|
||||
lists.Next()) {
|
||||
// Skip child lists that contain out-of-flow frames, we'll visit them by
|
||||
// following placeholders in the in-flow lists so that we visit these
|
||||
// frames in DOM order.
|
||||
// XXX do we actually need to exclude kOverflowOutOfFlowList too?
|
||||
if (lists.CurrentID() == FrameChildListID::kAbsoluteList ||
|
||||
lists.CurrentID() == FrameChildListID::kFixedList ||
|
||||
lists.CurrentID() == FrameChildListID::kFloatList ||
|
||||
lists.CurrentID() == FrameChildListID::kOverflowOutOfFlowList) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Search the child list, and return if we selected an anchor
|
||||
if (nsIFrame* anchor = FindAnchorInList(lists.CurrentList())) {
|
||||
return anchor;
|
||||
}
|
||||
}
|
||||
|
||||
// The spec requires us to do an extra pass to visit absolutely positioned
|
||||
// frames a second time after all the children of their containing block have
|
||||
// been visited.
|
||||
//
|
||||
// It's not clear why this is needed [1], but it matches Blink's
|
||||
// implementation, and is needed for a WPT test.
|
||||
//
|
||||
// [1] https://github.com/w3c/csswg-drafts/issues/3465
|
||||
const nsFrameList& absPosList =
|
||||
aFrame->GetChildList(FrameChildListID::kAbsoluteList);
|
||||
if (nsIFrame* anchor = FindAnchorInList(absPosList)) {
|
||||
return anchor;
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
nsIFrame* ScrollAnchorContainer::FindAnchorInList(
|
||||
const nsFrameList& aFrameList) const {
|
||||
for (nsIFrame* child : aFrameList) {
|
||||
// If this is a placeholder, try to follow it to the out of flow frame.
|
||||
nsIFrame* realFrame = nsPlaceholderFrame::GetRealFrameFor(child);
|
||||
if (child != realFrame) {
|
||||
// If the out of flow frame is not a descendant of our scroll frame,
|
||||
// then it must have a different containing block and cannot be an
|
||||
// anchor node.
|
||||
if (!nsLayoutUtils::IsProperAncestorFrame(Frame(), realFrame)) {
|
||||
ANCHOR_LOG(
|
||||
"\t\tSkipping out of flow frame that is not a descendant of the "
|
||||
"scroll frame.\n");
|
||||
continue;
|
||||
}
|
||||
ANCHOR_LOG("\t\tFollowing placeholder to out of flow frame.\n");
|
||||
child = realFrame;
|
||||
}
|
||||
|
||||
// Perform the candidate examination algorithm
|
||||
ExamineResult examine = ExamineAnchorCandidate(child);
|
||||
|
||||
// See the comment before the definition of `ExamineResult` in
|
||||
// `ScrollAnchorContainer.h` for an explanation of this behavior.
|
||||
switch (examine) {
|
||||
case ExamineResult::Exclude: {
|
||||
continue;
|
||||
}
|
||||
case ExamineResult::PassThrough: {
|
||||
nsIFrame* candidate = FindAnchorIn(child);
|
||||
if (!candidate) {
|
||||
continue;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
case ExamineResult::Traverse: {
|
||||
nsIFrame* candidate = FindAnchorIn(child);
|
||||
if (!candidate) {
|
||||
return child;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
case ExamineResult::Accept: {
|
||||
return child;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
} // namespace layout
|
||||
} // namespace mozilla
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
#ifndef mozilla_layout_ScrollAnchorContainer_h_
|
||||
#define mozilla_layout_ScrollAnchorContainer_h_
|
||||
|
||||
#include "nsPoint.h"
|
||||
|
||||
class nsIFrame;
|
||||
namespace mozilla {
|
||||
class ScrollFrameHelper;
|
||||
} // namespace mozilla
|
||||
|
@ -32,6 +35,12 @@ class ScrollAnchorContainer final {
|
|||
*/
|
||||
static ScrollAnchorContainer* FindFor(nsIFrame* aFrame);
|
||||
|
||||
/**
|
||||
* Returns the frame that is the selected anchor node or null if no anchor
|
||||
* is selected.
|
||||
*/
|
||||
nsIFrame* AnchorNode() const { return mAnchorNode; }
|
||||
|
||||
/**
|
||||
* Returns the frame that owns this scroll anchor container. This is always
|
||||
* non-null.
|
||||
|
@ -44,9 +53,80 @@ class ScrollAnchorContainer final {
|
|||
*/
|
||||
nsIScrollableFrame* ScrollableFrame() const;
|
||||
|
||||
/**
|
||||
* Find a suitable anchor node among the descendants of the scrollable frame.
|
||||
* This should only be called after the scroll anchor has been invalidated.
|
||||
*/
|
||||
void SelectAnchor();
|
||||
|
||||
/**
|
||||
* Notify the scroll anchor container that its scroll frame has been
|
||||
* scrolled by a user and should invalidate itself.
|
||||
*/
|
||||
void UserScrolled();
|
||||
|
||||
/**
|
||||
* Notify this scroll anchor container that its anchor node should be
|
||||
* invalidated and recomputed at the next available opportunity.
|
||||
*/
|
||||
void InvalidateAnchor();
|
||||
|
||||
/**
|
||||
* Notify this scroll anchor container that it will be destroyed along with
|
||||
* its parent frame.
|
||||
*/
|
||||
void Destroy();
|
||||
|
||||
private:
|
||||
// Represents an assessment of a frame's suitability as a scroll anchor,
|
||||
// from the scroll-anchoring spec's "candidate examination algorithm":
|
||||
// https://drafts.csswg.org/css-scroll-anchoring-1/#candidate-examination
|
||||
enum class ExamineResult {
|
||||
// The frame is an excluded subtree or fully clipped and should be ignored.
|
||||
// This corresponds with step 1 in the algorithm.
|
||||
Exclude,
|
||||
// This frame is an anonymous or inline box and its descendants should be
|
||||
// searched to find an anchor node. If none are found, then continue
|
||||
// searching. This is implied by the prologue of the algorithm, and
|
||||
// should be made explicit in the spec [1].
|
||||
//
|
||||
// [1] https://github.com/w3c/csswg-drafts/issues/3489
|
||||
PassThrough,
|
||||
// The frame is partially visible and its descendants should be searched to
|
||||
// find an anchor node. If none are found then this frame should be
|
||||
// selected. This corresponds with step 3 in the algorithm.
|
||||
Traverse,
|
||||
// The frame is fully visible and should be selected as an anchor node. This
|
||||
// corresponds with step 2 in the algorithm.
|
||||
Accept,
|
||||
};
|
||||
|
||||
ExamineResult ExamineAnchorCandidate(nsIFrame* aPrimaryFrame) const;
|
||||
|
||||
// Search a frame's children to find an anchor node. Returns the frame for a
|
||||
// valid anchor node, if one was found in the frames descendants, or null
|
||||
// otherwise.
|
||||
nsIFrame* FindAnchorIn(nsIFrame* aFrame) const;
|
||||
|
||||
// Search a child list to find an anchor node. Returns the frame for a valid
|
||||
// anchor node, if one was found in this child list, or null otherwise.
|
||||
nsIFrame* FindAnchorInList(const nsFrameList& aFrameList) const;
|
||||
|
||||
// The owner of this scroll anchor container
|
||||
ScrollFrameHelper* mScrollFrame;
|
||||
|
||||
// The anchor node that we will scroll to keep in the same relative position
|
||||
// after reflows. This may be null if we were not able to select a valid
|
||||
// scroll anchor
|
||||
nsIFrame* mAnchorNode;
|
||||
|
||||
// The last position of the scroll anchor node relative to the scrollable
|
||||
// frame. This is used for calculating the distance to scroll to keep the
|
||||
// anchor node in the same relative position
|
||||
nsPoint mLastAnchorPos;
|
||||
|
||||
// True if we should recalculate our anchor node at the next chance
|
||||
bool mAnchorNodeIsDirty : 1;
|
||||
};
|
||||
|
||||
} // namespace layout
|
||||
|
|
|
@ -111,6 +111,7 @@
|
|||
#include "mozilla/dom/TouchEvent.h"
|
||||
#include "mozilla/gfx/Tools.h"
|
||||
#include "mozilla/layers/WebRenderUserData.h"
|
||||
#include "mozilla/layout/ScrollAnchorContainer.h"
|
||||
#include "nsPrintfCString.h"
|
||||
#include "ActiveLayerTracker.h"
|
||||
|
||||
|
@ -727,6 +728,11 @@ void nsFrame::DestroyFrom(nsIFrame* aDestructRoot,
|
|||
ActiveLayerTracker::TransferActivityToContent(this, mContent);
|
||||
}
|
||||
|
||||
ScrollAnchorContainer* anchor = nullptr;
|
||||
if (IsScrollAnchor(&anchor)) {
|
||||
anchor->InvalidateAnchor();
|
||||
}
|
||||
|
||||
if (HasCSSAnimations() || HasCSSTransitions() ||
|
||||
EffectSet::GetEffectSet(this)) {
|
||||
// If no new frame for this element is created by the end of the
|
||||
|
@ -9143,6 +9149,28 @@ void nsIFrame::ComputePreserve3DChildrenOverflow(
|
|||
}
|
||||
}
|
||||
|
||||
bool nsIFrame::IsScrollAnchor(ScrollAnchorContainer** aOutContainer) {
|
||||
if (!mInScrollAnchorChain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ScrollAnchorContainer* container = ScrollAnchorContainer::FindFor(this);
|
||||
if (container->AnchorNode() != this) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (aOutContainer) {
|
||||
*aOutContainer = container;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool nsIFrame::IsInScrollAnchorChain() const { return mInScrollAnchorChain; }
|
||||
|
||||
void nsIFrame::SetInScrollAnchorChain(bool aInChain) {
|
||||
mInScrollAnchorChain = aInChain;
|
||||
}
|
||||
|
||||
uint32_t nsIFrame::GetDepthInFrameTree() const {
|
||||
uint32_t result = 0;
|
||||
for (nsContainerFrame* ancestor = GetParent(); ancestor;
|
||||
|
|
|
@ -2741,6 +2741,7 @@ void ScrollFrameHelper::ScrollToImpl(nsPoint aPt, const nsRect& aRange,
|
|||
}
|
||||
|
||||
ScrollVisual();
|
||||
mAnchor.UserScrolled();
|
||||
|
||||
bool schedulePaint = true;
|
||||
if (nsLayoutUtils::AsyncPanZoomEnabled(mOuter) &&
|
||||
|
@ -4711,6 +4712,8 @@ void ScrollFrameHelper::AppendAnonymousContentTo(
|
|||
}
|
||||
|
||||
void ScrollFrameHelper::Destroy(PostDestroyData& aPostDestroyData) {
|
||||
mAnchor.Destroy();
|
||||
|
||||
if (mScrollbarActivity) {
|
||||
mScrollbarActivity->Destroy();
|
||||
mScrollbarActivity = nullptr;
|
||||
|
|
|
@ -115,6 +115,10 @@ class Layer;
|
|||
class LayerManager;
|
||||
} // namespace layers
|
||||
|
||||
namespace layout {
|
||||
class ScrollAnchorContainer;
|
||||
} // namespace layout
|
||||
|
||||
namespace dom {
|
||||
class Selection;
|
||||
} // namespace dom
|
||||
|
@ -566,7 +570,8 @@ class nsIFrame : public nsQueryFrame {
|
|||
mIsPrimaryFrame(false),
|
||||
mMayHaveTransformAnimation(false),
|
||||
mMayHaveOpacityAnimation(false),
|
||||
mAllDescendantsAreInvisible(false) {
|
||||
mAllDescendantsAreInvisible(false),
|
||||
mInScrollAnchorChain(false) {
|
||||
mozilla::PodZero(&mOverflow);
|
||||
}
|
||||
|
||||
|
@ -1846,6 +1851,24 @@ class nsIFrame : public nsQueryFrame {
|
|||
|
||||
void RecomputePerspectiveChildrenOverflow(const nsIFrame* aStartFrame);
|
||||
|
||||
/**
|
||||
* Returns whether this frame is the anchor of some ancestor scroll frame. As
|
||||
* this frame is moved, the scroll frame will apply adjustments to keep this
|
||||
* scroll frame in the same relative position.
|
||||
*
|
||||
* aOutContainer will optionally be set to the scroll anchor container for
|
||||
* this frame if this frame is an anchor.
|
||||
*/
|
||||
bool IsScrollAnchor(
|
||||
mozilla::layout::ScrollAnchorContainer** aOutContainer = nullptr);
|
||||
|
||||
/**
|
||||
* Returns whether this frame is the anchor of some ancestor scroll frame, or
|
||||
* has a descendant which is the scroll anchor.
|
||||
*/
|
||||
bool IsInScrollAnchorChain() const;
|
||||
void SetInScrollAnchorChain(bool aInChain);
|
||||
|
||||
/**
|
||||
* Returns the number of ancestors between this and the root of our frame tree
|
||||
*/
|
||||
|
@ -4293,9 +4316,12 @@ class nsIFrame : public nsQueryFrame {
|
|||
*/
|
||||
bool mAllDescendantsAreInvisible : 1;
|
||||
|
||||
protected:
|
||||
// There is a 1-bit gap left here.
|
||||
/**
|
||||
* True if we are or contain the scroll anchor for a scrollable frame.
|
||||
*/
|
||||
bool mInScrollAnchorChain : 1;
|
||||
|
||||
protected:
|
||||
// Helpers
|
||||
/**
|
||||
* Can we stop inside this frame when we're skipping non-rendered whitespace?
|
||||
|
|
Загрузка…
Ссылка в новой задаче