Bug 886646 - Part 6: Implement sticky positioning, calculated on reflow and scroll. r=dbaron, r=dholbert

This commit is contained in:
Corey Ford 2013-09-06 09:35:16 -04:00
parent 84181a5ffd
commit b3400abc97
10 changed files with 417 additions and 12 deletions

View File

@ -29,6 +29,7 @@
#include "nsViewportFrame.h"
#include "nsSVGTextFrame2.h"
#include "nsSVGTextPathFrame.h"
#include "StickyScrollContainer.h"
#include "nsIRootBox.h"
#include "nsIDOMMutationEvent.h"
#include "nsContentUtils.h"
@ -326,7 +327,7 @@ RestyleManager::RecomputePosition(nsIFrame* aFrame)
aFrame->SchedulePaint();
// For relative positioning, we can simply update the frame rect
if (display->mPosition == NS_STYLE_POSITION_RELATIVE) {
if (display->IsRelativelyPositionedStyle()) {
switch (display->mDisplay) {
case NS_STYLE_DISPLAY_TABLE_CAPTION:
case NS_STYLE_DISPLAY_TABLE_CELL:
@ -345,17 +346,27 @@ RestyleManager::RecomputePosition(nsIFrame* aFrame)
}
nsIFrame* cb = aFrame->GetContainingBlock();
const nsSize size = cb->GetContentRectRelativeToSelf().Size();
nsPoint position = aFrame->GetNormalPosition();
nsMargin newOffsets;
// Move the frame
nsHTMLReflowState::ComputeRelativeOffsets(
cb->StyleVisibility()->mDirection,
aFrame, size.width, size.height, newOffsets);
NS_ASSERTION(newOffsets.left == -newOffsets.right &&
newOffsets.top == -newOffsets.bottom,
"ComputeRelativeOffsets should return valid results");
if (display->mPosition == NS_STYLE_POSITION_STICKY) {
StickyScrollContainer::ComputeStickyOffsets(aFrame);
} else {
MOZ_ASSERT(NS_STYLE_POSITION_RELATIVE == display->mPosition,
"Unexpected type of positioning");
const nsSize size = cb->GetContentRectRelativeToSelf().Size();
nsHTMLReflowState::ComputeRelativeOffsets(
cb->StyleVisibility()->mDirection,
aFrame, size.width, size.height, newOffsets);
NS_ASSERTION(newOffsets.left == -newOffsets.right &&
newOffsets.top == -newOffsets.bottom,
"ComputeRelativeOffsets should return valid results");
}
nsPoint position = aFrame->GetNormalPosition();
// This handles both relative and sticky positioning.
nsHTMLReflowState::ApplyRelativePositioning(aFrame, newOffsets, &position);
aFrame->SetPosition(position);

View File

@ -29,6 +29,10 @@ class OverflowChangedTracker
{
public:
OverflowChangedTracker() :
mSubtreeRoot(nullptr)
{}
~OverflowChangedTracker()
{
NS_ASSERTION(mEntryList.empty(), "Need to flush before destroying!");
@ -67,6 +71,15 @@ public:
}
}
/**
* Set the subtree root to limit overflow updates. This must be set if and
* only if currently reflowing aSubtreeRoot, to ensure overflow changes will
* still propagate correctly.
*/
void SetSubtreeRoot(const nsIFrame* aSubtreeRoot) {
mSubtreeRoot = aSubtreeRoot;
}
/**
* Update the overflow of all added frames, and clear the entry list.
*
@ -100,7 +113,7 @@ public:
}
if (updateParent) {
nsIFrame *parent = frame->GetParent();
if (parent) {
if (parent && parent != mSubtreeRoot) {
if (!mEntryList.contains(Entry(parent, entry->mDepth - 1, false))) {
mEntryList.insert(new Entry(parent, entry->mDepth - 1, false));
}
@ -165,6 +178,9 @@ private:
/* A list of frames to process, sorted by their depth in the frame tree */
SplayTree<Entry, Entry> mEntryList;
/* Don't update overflow of this frame or its ancestors. */
const nsIFrame* mSubtreeRoot;
};
class RestyleTracker {

View File

@ -0,0 +1,252 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sts=2 et sw=2 tw=80: */
/* 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/. */
/**
* compute sticky positioning, both during reflow and when the scrolling
* container scrolls
*/
#include "StickyScrollContainer.h"
#include "nsIFrame.h"
#include "nsIScrollableFrame.h"
#include "nsLayoutUtils.h"
#include "RestyleTracker.h"
using namespace mozilla::css;
namespace mozilla {
void DestroyStickyScrollContainer(void* aPropertyValue)
{
delete static_cast<StickyScrollContainer*>(aPropertyValue);
}
NS_DECLARE_FRAME_PROPERTY(StickyScrollContainerProperty,
DestroyStickyScrollContainer)
StickyScrollContainer::StickyScrollContainer(nsIScrollableFrame* aScrollFrame)
: mScrollFrame(aScrollFrame)
, mScrollPosition()
{
mScrollFrame->AddScrollPositionListener(this);
}
StickyScrollContainer::~StickyScrollContainer()
{
mScrollFrame->RemoveScrollPositionListener(this);
}
// static
StickyScrollContainer*
StickyScrollContainer::StickyScrollContainerForFrame(nsIFrame* aFrame)
{
nsIScrollableFrame* scrollFrame =
nsLayoutUtils::GetNearestScrollableFrame(aFrame->GetParent(),
nsLayoutUtils::SCROLLABLE_SAME_DOC |
nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
NS_ASSERTION(scrollFrame, "Need a scrolling container");
FrameProperties props = static_cast<nsIFrame*>(do_QueryFrame(scrollFrame))->
Properties();
StickyScrollContainer* s = static_cast<StickyScrollContainer*>
(props.Get(StickyScrollContainerProperty()));
if (!s) {
s = new StickyScrollContainer(scrollFrame);
props.Set(StickyScrollContainerProperty(), s);
}
return s;
}
// static
StickyScrollContainer*
StickyScrollContainer::GetStickyScrollContainerForScrollFrame(nsIFrame* aFrame)
{
FrameProperties props = aFrame->Properties();
return static_cast<StickyScrollContainer*>
(props.Get(StickyScrollContainerProperty()));
}
static nscoord
ComputeStickySideOffset(Side aSide, const nsStyleSides& aOffset,
nscoord aPercentBasis)
{
if (eStyleUnit_Auto == aOffset.GetUnit(aSide)) {
return NS_AUTOOFFSET;
} else {
return nsLayoutUtils::ComputeCBDependentValue(aPercentBasis,
aOffset.Get(aSide));
}
}
// static
void
StickyScrollContainer::ComputeStickyOffsets(nsIFrame* aFrame)
{
nsIScrollableFrame* scrollableFrame =
nsLayoutUtils::GetNearestScrollableFrame(aFrame->GetParent(),
nsLayoutUtils::SCROLLABLE_SAME_DOC |
nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
if (!scrollableFrame) {
// Not sure how this would happen, but bail if it does.
NS_ERROR("Couldn't find a scrollable frame");
return;
}
nsSize scrollContainerSize = scrollableFrame->GetScrolledFrame()->
GetContentRectRelativeToSelf().Size();
nsMargin computedOffsets;
const nsStylePosition* position = aFrame->StylePosition();
computedOffsets.left = ComputeStickySideOffset(eSideLeft, position->mOffset,
scrollContainerSize.width);
computedOffsets.right = ComputeStickySideOffset(eSideRight, position->mOffset,
scrollContainerSize.width);
computedOffsets.top = ComputeStickySideOffset(eSideTop, position->mOffset,
scrollContainerSize.height);
computedOffsets.bottom = ComputeStickySideOffset(eSideBottom, position->mOffset,
scrollContainerSize.height);
// Store the offset
FrameProperties props = aFrame->Properties();
nsMargin* offsets = static_cast<nsMargin*>
(props.Get(nsIFrame::ComputedStickyOffsetProperty()));
if (offsets) {
*offsets = computedOffsets;
} else {
props.Set(nsIFrame::ComputedStickyOffsetProperty(),
new nsMargin(computedOffsets));
}
}
void
StickyScrollContainer::ComputeStickyLimits(nsIFrame* aFrame, nsRect* aStick,
nsRect* aContain) const
{
aStick->SetRect(nscoord_MIN/2, nscoord_MIN/2, nscoord_MAX, nscoord_MAX);
aContain->SetRect(nscoord_MIN/2, nscoord_MIN/2, nscoord_MAX, nscoord_MAX);
const nsMargin* computedOffsets = static_cast<nsMargin*>(
aFrame->Properties().Get(nsIFrame::ComputedStickyOffsetProperty()));
if (!computedOffsets) {
// We haven't reflowed the scroll frame yet, so offsets haven't been
// computed. Bail.
return;
}
nsIFrame* scrolledFrame = mScrollFrame->GetScrolledFrame();
nsIFrame* cbFrame = aFrame->GetContainingBlock();
NS_ASSERTION(cbFrame == scrolledFrame ||
nsLayoutUtils::IsProperAncestorFrame(scrolledFrame, cbFrame),
"Scroll frame should be an ancestor of the containing block");
nsRect rect = aFrame->GetRect();
nsMargin margin = aFrame->GetUsedMargin();
// Containing block limits
if (cbFrame != scrolledFrame) {
nsMargin cbBorderPadding = cbFrame->GetUsedBorderAndPadding();
aContain->SetRect(nsPoint(cbBorderPadding.left, cbBorderPadding.top) -
aFrame->GetParent()->GetOffsetTo(cbFrame),
cbFrame->GetContentRectRelativeToSelf().Size() -
rect.Size());
aContain->Deflate(margin);
}
nsMargin sfPadding = scrolledFrame->GetUsedPadding();
nsPoint sfOffset = aFrame->GetParent()->GetOffsetTo(scrolledFrame);
// Top
if (computedOffsets->top != NS_AUTOOFFSET) {
aStick->SetTopEdge(mScrollPosition.y + sfPadding.top +
computedOffsets->top - sfOffset.y);
}
nsSize sfSize = scrolledFrame->GetContentRectRelativeToSelf().Size();
// Bottom
if (computedOffsets->bottom != NS_AUTOOFFSET &&
(computedOffsets->top == NS_AUTOOFFSET ||
rect.height <= sfSize.height - computedOffsets->TopBottom())) {
aStick->SetBottomEdge(mScrollPosition.y + sfPadding.top + sfSize.height -
computedOffsets->bottom - rect.height - sfOffset.y);
}
uint8_t direction = cbFrame->StyleVisibility()->mDirection;
// Left
if (computedOffsets->left != NS_AUTOOFFSET &&
(computedOffsets->right == NS_AUTOOFFSET ||
direction == NS_STYLE_DIRECTION_LTR ||
rect.width <= sfSize.width - computedOffsets->LeftRight())) {
aStick->SetLeftEdge(mScrollPosition.x + sfPadding.left +
computedOffsets->left - sfOffset.x);
}
// Right
if (computedOffsets->right != NS_AUTOOFFSET &&
(computedOffsets->left == NS_AUTOOFFSET ||
direction == NS_STYLE_DIRECTION_RTL ||
rect.width <= sfSize.width - computedOffsets->LeftRight())) {
aStick->SetRightEdge(mScrollPosition.x + sfPadding.left + sfSize.width -
computedOffsets->right - rect.width - sfOffset.x);
}
}
nsPoint
StickyScrollContainer::ComputePosition(nsIFrame* aFrame) const
{
nsRect stick;
nsRect contain;
ComputeStickyLimits(aFrame, &stick, &contain);
nsPoint position = aFrame->GetNormalPosition();
// For each sticky direction (top, bottom, left, right), move the frame along
// the appropriate axis, based on the scroll position, but limit this to keep
// the element's margin box within the containing block.
position.y = std::max(position.y, std::min(stick.y, contain.YMost()));
position.y = std::min(position.y, std::max(stick.YMost(), contain.y));
position.x = std::max(position.x, std::min(stick.x, contain.XMost()));
position.x = std::min(position.x, std::max(stick.XMost(), contain.x));
return position;
}
void
StickyScrollContainer::UpdatePositions(nsPoint aScrollPosition,
nsIFrame* aSubtreeRoot)
{
NS_ASSERTION(!aSubtreeRoot || aSubtreeRoot == do_QueryFrame(mScrollFrame),
"If reflowing, should be reflowing the scroll frame");
mScrollPosition = aScrollPosition;
OverflowChangedTracker oct;
oct.SetSubtreeRoot(aSubtreeRoot);
for (nsTArray<nsIFrame*>::size_type i = 0; i < mFrames.Length(); i++) {
nsIFrame* f = mFrames[i];
if (aSubtreeRoot) {
// Reflowing the scroll frame, so recompute offsets.
ComputeStickyOffsets(f);
}
f->SetPosition(ComputePosition(f));
oct.AddFrame(f);
}
oct.Flush();
}
void
StickyScrollContainer::ScrollPositionWillChange(nscoord aX, nscoord aY)
{
}
void
StickyScrollContainer::ScrollPositionDidChange(nscoord aX, nscoord aY)
{
UpdatePositions(nsPoint(aX, aY), nullptr);
}
} // namespace mozilla

View File

@ -0,0 +1,90 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sts=2 et sw=2 tw=80: */
/* 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/. */
/**
* compute sticky positioning, both during reflow and when the scrolling
* container scrolls
*/
#ifndef StickyScrollContainer_h
#define StickyScrollContainer_h
#include "nsPoint.h"
#include "nsTArray.h"
#include "nsIScrollPositionListener.h"
class nsRect;
class nsIFrame;
class nsIScrollableFrame;
namespace mozilla {
class StickyScrollContainer MOZ_FINAL : public nsIScrollPositionListener
{
public:
/**
* Find the StickyScrollContainer associated with the scroll container of
* the given frame, creating it if necessary.
*/
static StickyScrollContainer* StickyScrollContainerForFrame(nsIFrame* aFrame);
/**
* Find the StickyScrollContainer associated with the given scroll frame,
* if it exists.
*/
static StickyScrollContainer* GetStickyScrollContainerForScrollFrame(nsIFrame* aScrollFrame);
void AddFrame(nsIFrame* aFrame) {
mFrames.AppendElement(aFrame);
}
void RemoveFrame(nsIFrame* aFrame) {
mFrames.RemoveElement(aFrame);
}
// Compute the offsets for a sticky position element
static void ComputeStickyOffsets(nsIFrame* aFrame);
/**
* Compute the position of a sticky positioned frame, based on information
* stored in its properties along with our scroll frame and scroll position.
*/
nsPoint ComputePosition(nsIFrame* aFrame) const;
/**
* Compute and set the position of all sticky frames, given the current
* scroll position of the scroll frame. If not in reflow, aSubtreeRoot should
* be null; otherwise, overflow-area updates will be limited to not affect
* aSubtreeRoot or its ancestors.
*/
void UpdatePositions(nsPoint aScrollPosition, nsIFrame* aSubtreeRoot);
// nsIScrollPositionListener
virtual void ScrollPositionWillChange(nscoord aX, nscoord aY) MOZ_OVERRIDE;
virtual void ScrollPositionDidChange(nscoord aX, nscoord aY) MOZ_OVERRIDE;
private:
StickyScrollContainer(nsIScrollableFrame* aScrollFrame);
~StickyScrollContainer();
/**
* Compute two rectangles that determine sticky positioning: |aStick|, based
* on the scroll container, and |aContain|, based on the containing block.
* Sticky positioning keeps the frame position (its upper-left corner) always
* within |aContain| and secondarily within |aStick|.
*/
void ComputeStickyLimits(nsIFrame* aFrame, nsRect* aStick,
nsRect* aContain) const;
friend void DestroyStickyScrollContainer(void* aPropertyValue);
nsIScrollableFrame* const mScrollFrame;
nsTArray<nsIFrame*> mFrames;
nsPoint mScrollPosition;
};
} // namespace mozilla
#endif /* StickyScrollContainer_h */

View File

@ -45,6 +45,7 @@ EXPORTS.mozilla.layout += [
CPP_SOURCES += [
'FrameChildList.cpp',
'ScrollbarActivity.cpp',
'StickyScrollContainer.cpp',
'TextOverflow.cpp',
'nsAbsoluteContainingBlock.cpp',
'nsBRFrame.cpp',

View File

@ -73,6 +73,7 @@
#include "gfxContext.h"
#include "nsRenderingContext.h"
#include "nsAbsoluteContainingBlock.h"
#include "StickyScrollContainer.h"
#include "nsFontInflationData.h"
#include "gfxASurface.h"
#include "nsRegion.h"
@ -510,6 +511,9 @@ nsFrame::Init(nsIContent* aContent,
// property, so we can set this bit here and then ignore it.
mState |= NS_FRAME_MAY_BE_TRANSFORMED;
}
if (disp->mPosition == NS_STYLE_POSITION_STICKY) {
StickyScrollContainer::StickyScrollContainerForFrame(this)->AddFrame(this);
}
if (nsLayoutUtils::FontSizeInflationEnabled(PresContext()) || !GetParent()
#ifdef DEBUG
@ -588,6 +592,11 @@ nsFrame::DestroyFrom(nsIFrame* aDestructRoot)
nsSVGEffects::InvalidateDirectRenderingObservers(this);
if (StyleDisplay()->mPosition == NS_STYLE_POSITION_STICKY) {
StickyScrollContainer::StickyScrollContainerForFrame(this)->
RemoveFrame(this);
}
// Get the view pointer now before the frame properties disappear
// when we call NotifyDestroyingFrame()
nsView* view = GetView();

View File

@ -48,6 +48,7 @@
#include "nsThemeConstants.h"
#include "nsSVGIntegrationUtils.h"
#include "nsIScrollPositionListener.h"
#include "StickyScrollContainer.h"
#include <algorithm>
#include <cstdlib> // for std::abs(int/long)
#include <cmath> // for std::abs(float/double)
@ -827,6 +828,7 @@ nsHTMLScrollFrame::Reflow(nsPresContext* aPresContext,
state.mContentsOverflowAreas + mInner.mScrolledFrame->GetPosition());
}
mInner.UpdateSticky();
FinishReflowWithAbsoluteFrames(aPresContext, aDesiredSize, aReflowState, aStatus);
if (!InInitialReflow() && !mInner.mHadNonInitialReflow) {
@ -3544,6 +3546,8 @@ nsXULScrollFrame::Layout(nsBoxLayoutState& aState)
mInner.mHadNonInitialReflow = true;
}
mInner.UpdateSticky();
// Set up overflow areas for block frames for the benefit of
// text-overflow.
nsIFrame* f = mInner.mScrolledFrame->GetContentInsertionFrame();
@ -3713,6 +3717,17 @@ nsGfxScrollFrameInner::UpdateOverflow()
return mOuter->nsContainerFrame::UpdateOverflow();
}
void
nsGfxScrollFrameInner::UpdateSticky()
{
StickyScrollContainer* ssc = StickyScrollContainer::
GetStickyScrollContainerForScrollFrame(mOuter);
if (ssc) {
nsIScrollableFrame* scrollFrame = do_QueryFrame(mOuter);
ssc->UpdatePositions(scrollFrame->GetScrollPosition(), mOuter);
}
}
void
nsGfxScrollFrameInner::AdjustScrollbarRectForResizer(
nsIFrame* aFrame, nsPresContext* aPresContext,

View File

@ -276,6 +276,8 @@ public:
bool UpdateOverflow();
void UpdateSticky();
// adjust the scrollbar rectangle aRect to account for any visible resizer.
// aHasResizer specifies if there is a content resizer, however this method
// will also check if a widget resizer is present as well.

View File

@ -23,6 +23,7 @@
#include "nsLayoutUtils.h"
#include "mozilla/Preferences.h"
#include "nsFontInflationData.h"
#include "StickyScrollContainer.h"
#include <algorithm>
#ifdef DEBUG
@ -32,6 +33,7 @@
#endif
using namespace mozilla;
using namespace mozilla::css;
using namespace mozilla::layout;
enum eNormalLineHeightControl {
@ -842,6 +844,9 @@ nsHTMLReflowState::ApplyRelativePositioning(nsIFrame* aFrame,
const nsStyleDisplay* display = aFrame->StyleDisplay();
if (NS_STYLE_POSITION_RELATIVE == display->mPosition) {
*aPosition += nsPoint(aComputedOffsets.left, aComputedOffsets.top);
} else if (NS_STYLE_POSITION_STICKY == display->mPosition) {
*aPosition = StickyScrollContainer::StickyScrollContainerForFrame(aFrame)->
ComputePosition(aFrame);
}
}
@ -1940,8 +1945,11 @@ nsHTMLReflowState::InitConstraints(nsPresContext* aPresContext,
// Compute our offsets if the element is relatively positioned. We need
// the correct containing block width and height here, which is why we need
// to do it after all the quirks-n-such above.
if (mStyleDisplay->IsRelativelyPositioned(frame)) {
// to do it after all the quirks-n-such above. (If the element is sticky
// positioned, we need to wait until the scroll container knows its size,
// so we compute offsets from StickyScrollContainer::UpdatePositions.)
if (mStyleDisplay->IsRelativelyPositioned(frame) &&
NS_STYLE_POSITION_RELATIVE == mStyleDisplay->mPosition) {
uint8_t direction = NS_STYLE_DIRECTION_LTR;
if (cbrs && NS_STYLE_DIRECTION_RTL == cbrs->mStyleVisibility->mDirection) {
direction = NS_STYLE_DIRECTION_RTL;

View File

@ -927,6 +927,7 @@ public:
NS_DECLARE_FRAME_PROPERTY(IBSplitSpecialPrevSibling, nullptr)
NS_DECLARE_FRAME_PROPERTY(NormalPositionProperty, DestroyPoint)
NS_DECLARE_FRAME_PROPERTY(ComputedStickyOffsetProperty, DestroyMargin)
NS_DECLARE_FRAME_PROPERTY(OutlineInnerRectProperty, DestroyRect)
NS_DECLARE_FRAME_PROPERTY(PreEffectsBBoxProperty, DestroyRect)