layout/generic/StickyScrollContainer.cpp

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     2 /* vim: set ts=2 sts=2 et sw=2 tw=80: */
     3 /* This Source Code Form is subject to the terms of the Mozilla Public
     4  * License, v. 2.0. If a copy of the MPL was not distributed with this
     5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     7 /**
     8  * compute sticky positioning, both during reflow and when the scrolling
     9  * container scrolls
    10  */
    12 #include "StickyScrollContainer.h"
    13 #include "nsIFrame.h"
    14 #include "nsIScrollableFrame.h"
    15 #include "nsLayoutUtils.h"
    16 #include "RestyleTracker.h"
    18 using namespace mozilla::css;
    20 namespace mozilla {
    22 void DestroyStickyScrollContainer(void* aPropertyValue)
    23 {
    24   delete static_cast<StickyScrollContainer*>(aPropertyValue);
    25 }
    27 NS_DECLARE_FRAME_PROPERTY(StickyScrollContainerProperty,
    28                           DestroyStickyScrollContainer)
    30 StickyScrollContainer::StickyScrollContainer(nsIScrollableFrame* aScrollFrame)
    31   : mScrollFrame(aScrollFrame)
    32   , mScrollPosition()
    33 {
    34   mScrollFrame->AddScrollPositionListener(this);
    35 }
    37 StickyScrollContainer::~StickyScrollContainer()
    38 {
    39   mScrollFrame->RemoveScrollPositionListener(this);
    40 }
    42 // static
    43 StickyScrollContainer*
    44 StickyScrollContainer::GetStickyScrollContainerForFrame(nsIFrame* aFrame)
    45 {
    46   nsIScrollableFrame* scrollFrame =
    47     nsLayoutUtils::GetNearestScrollableFrame(aFrame->GetParent(),
    48       nsLayoutUtils::SCROLLABLE_SAME_DOC |
    49       nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
    50   if (!scrollFrame) {
    51     // We might not find any, for instance in the case of
    52     // <html style="position: fixed">
    53     return nullptr;
    54   }
    55   FrameProperties props = static_cast<nsIFrame*>(do_QueryFrame(scrollFrame))->
    56     Properties();
    57   StickyScrollContainer* s = static_cast<StickyScrollContainer*>
    58     (props.Get(StickyScrollContainerProperty()));
    59   if (!s) {
    60     s = new StickyScrollContainer(scrollFrame);
    61     props.Set(StickyScrollContainerProperty(), s);
    62   }
    63   return s;
    64 }
    66 // static
    67 void
    68 StickyScrollContainer::NotifyReparentedFrameAcrossScrollFrameBoundary(nsIFrame* aFrame,
    69                                                                       nsIFrame* aOldParent)
    70 {
    71   nsIScrollableFrame* oldScrollFrame =
    72     nsLayoutUtils::GetNearestScrollableFrame(aOldParent,
    73       nsLayoutUtils::SCROLLABLE_SAME_DOC |
    74       nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
    75   if (!oldScrollFrame) {
    76     // XXX maybe aFrame has sticky descendants that can be sticky now, but
    77     // we aren't going to handle that.
    78     return;
    79   }
    80   FrameProperties props = static_cast<nsIFrame*>(do_QueryFrame(oldScrollFrame))->
    81     Properties();
    82   StickyScrollContainer* oldSSC = static_cast<StickyScrollContainer*>
    83     (props.Get(StickyScrollContainerProperty()));
    84   if (!oldSSC) {
    85     // aOldParent had no sticky descendants, so aFrame doesn't have any sticky
    86     // descendants, and we're done here.
    87     return;
    88   }
    90   auto i = oldSSC->mFrames.Length();
    91   while (i-- > 0) {
    92     nsIFrame* f = oldSSC->mFrames[i];
    93     StickyScrollContainer* newSSC = GetStickyScrollContainerForFrame(f);
    94     if (newSSC != oldSSC) {
    95       oldSSC->RemoveFrame(f);
    96       if (newSSC) {
    97         newSSC->AddFrame(f);
    98       }
    99     }
   100   }
   101 }
   103 // static
   104 StickyScrollContainer*
   105 StickyScrollContainer::GetStickyScrollContainerForScrollFrame(nsIFrame* aFrame)
   106 {
   107   FrameProperties props = aFrame->Properties();
   108   return static_cast<StickyScrollContainer*>
   109     (props.Get(StickyScrollContainerProperty()));
   110 }
   112 static nscoord
   113 ComputeStickySideOffset(Side aSide, const nsStyleSides& aOffset,
   114                         nscoord aPercentBasis)
   115 {
   116   if (eStyleUnit_Auto == aOffset.GetUnit(aSide)) {
   117     return NS_AUTOOFFSET;
   118   } else {
   119     return nsLayoutUtils::ComputeCBDependentValue(aPercentBasis,
   120                                                   aOffset.Get(aSide));
   121   }
   122 }
   124 // static
   125 void
   126 StickyScrollContainer::ComputeStickyOffsets(nsIFrame* aFrame)
   127 {
   128   nsIScrollableFrame* scrollableFrame =
   129     nsLayoutUtils::GetNearestScrollableFrame(aFrame->GetParent(),
   130       nsLayoutUtils::SCROLLABLE_SAME_DOC |
   131       nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
   133   if (!scrollableFrame) {
   134     // Bail.
   135     return;
   136   }
   138   nsSize scrollContainerSize = scrollableFrame->GetScrolledFrame()->
   139     GetContentRectRelativeToSelf().Size();
   141   nsMargin computedOffsets;
   142   const nsStylePosition* position = aFrame->StylePosition();
   144   computedOffsets.left   = ComputeStickySideOffset(eSideLeft, position->mOffset,
   145                                                    scrollContainerSize.width);
   146   computedOffsets.right  = ComputeStickySideOffset(eSideRight, position->mOffset,
   147                                                    scrollContainerSize.width);
   148   computedOffsets.top    = ComputeStickySideOffset(eSideTop, position->mOffset,
   149                                                    scrollContainerSize.height);
   150   computedOffsets.bottom = ComputeStickySideOffset(eSideBottom, position->mOffset,
   151                                                    scrollContainerSize.height);
   153   // Store the offset
   154   FrameProperties props = aFrame->Properties();
   155   nsMargin* offsets = static_cast<nsMargin*>
   156     (props.Get(nsIFrame::ComputedOffsetProperty()));
   157   if (offsets) {
   158     *offsets = computedOffsets;
   159   } else {
   160     props.Set(nsIFrame::ComputedOffsetProperty(),
   161               new nsMargin(computedOffsets));
   162   }
   163 }
   165 void
   166 StickyScrollContainer::ComputeStickyLimits(nsIFrame* aFrame, nsRect* aStick,
   167                                            nsRect* aContain) const
   168 {
   169   NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame),
   170                "Can't sticky position individual continuations");
   172   aStick->SetRect(nscoord_MIN/2, nscoord_MIN/2, nscoord_MAX, nscoord_MAX);
   173   aContain->SetRect(nscoord_MIN/2, nscoord_MIN/2, nscoord_MAX, nscoord_MAX);
   175   const nsMargin* computedOffsets = static_cast<nsMargin*>(
   176     aFrame->Properties().Get(nsIFrame::ComputedOffsetProperty()));
   177   if (!computedOffsets) {
   178     // We haven't reflowed the scroll frame yet, so offsets haven't been
   179     // computed. Bail.
   180     return;
   181   }
   183   nsIFrame* scrolledFrame = mScrollFrame->GetScrolledFrame();
   184   nsIFrame* cbFrame = aFrame->GetContainingBlock();
   185   NS_ASSERTION(cbFrame == scrolledFrame ||
   186     nsLayoutUtils::IsProperAncestorFrame(scrolledFrame, cbFrame),
   187     "Scroll frame should be an ancestor of the containing block");
   189   nsRect rect =
   190     nsLayoutUtils::GetAllInFlowRectsUnion(aFrame, aFrame->GetParent());
   192   // Containing block limits for the position of aFrame relative to its parent.
   193   // The margin box of the sticky element stays within the content box of the
   194   // contaning-block element.
   195   if (cbFrame != scrolledFrame) {
   196     *aContain = nsLayoutUtils::
   197       GetAllInFlowRectsUnion(cbFrame, aFrame->GetParent(),
   198                              nsLayoutUtils::RECTS_USE_CONTENT_BOX);
   199     nsRect marginRect = nsLayoutUtils::
   200       GetAllInFlowRectsUnion(aFrame, aFrame->GetParent(),
   201                              nsLayoutUtils::RECTS_USE_MARGIN_BOX);
   203     // Deflate aContain by the difference between the union of aFrame's
   204     // continuations' margin boxes and the union of their border boxes, so that
   205     // by keeping aFrame within aContain, we keep the union of the margin boxes
   206     // within the containing block's content box.
   207     aContain->Deflate(marginRect - rect);
   209     // Deflate aContain by the border-box size, to form a constraint on the
   210     // upper-left corner of aFrame and continuations.
   211     aContain->Deflate(nsMargin(0, rect.width, rect.height, 0));
   212   }
   214   nsMargin sfPadding = scrolledFrame->GetUsedPadding();
   215   nsPoint sfOffset = aFrame->GetParent()->GetOffsetTo(scrolledFrame);
   217   // Top
   218   if (computedOffsets->top != NS_AUTOOFFSET) {
   219     aStick->SetTopEdge(mScrollPosition.y + sfPadding.top +
   220                        computedOffsets->top - sfOffset.y);
   221   }
   223   nsSize sfSize = scrolledFrame->GetContentRectRelativeToSelf().Size();
   225   // Bottom
   226   if (computedOffsets->bottom != NS_AUTOOFFSET &&
   227       (computedOffsets->top == NS_AUTOOFFSET ||
   228        rect.height <= sfSize.height - computedOffsets->TopBottom())) {
   229     aStick->SetBottomEdge(mScrollPosition.y + sfPadding.top + sfSize.height -
   230                           computedOffsets->bottom - rect.height - sfOffset.y);
   231   }
   233   uint8_t direction = cbFrame->StyleVisibility()->mDirection;
   235   // Left
   236   if (computedOffsets->left != NS_AUTOOFFSET &&
   237       (computedOffsets->right == NS_AUTOOFFSET ||
   238        direction == NS_STYLE_DIRECTION_LTR ||
   239        rect.width <= sfSize.width - computedOffsets->LeftRight())) {
   240     aStick->SetLeftEdge(mScrollPosition.x + sfPadding.left +
   241                         computedOffsets->left - sfOffset.x);
   242   }
   244   // Right
   245   if (computedOffsets->right != NS_AUTOOFFSET &&
   246       (computedOffsets->left == NS_AUTOOFFSET ||
   247        direction == NS_STYLE_DIRECTION_RTL ||
   248        rect.width <= sfSize.width - computedOffsets->LeftRight())) {
   249     aStick->SetRightEdge(mScrollPosition.x + sfPadding.left + sfSize.width -
   250                          computedOffsets->right - rect.width - sfOffset.x);
   251   }
   253   // These limits are for the bounding box of aFrame's continuations. Convert
   254   // to limits for aFrame itself.
   255   nsPoint frameOffset = aFrame->GetPosition() - rect.TopLeft();
   256   aStick->MoveBy(frameOffset);
   257   aContain->MoveBy(frameOffset);
   258 }
   260 nsPoint
   261 StickyScrollContainer::ComputePosition(nsIFrame* aFrame) const
   262 {
   263   nsRect stick;
   264   nsRect contain;
   265   ComputeStickyLimits(aFrame, &stick, &contain);
   267   nsPoint position = aFrame->GetNormalPosition();
   269   // For each sticky direction (top, bottom, left, right), move the frame along
   270   // the appropriate axis, based on the scroll position, but limit this to keep
   271   // the element's margin box within the containing block.
   272   position.y = std::max(position.y, std::min(stick.y, contain.YMost()));
   273   position.y = std::min(position.y, std::max(stick.YMost(), contain.y));
   274   position.x = std::max(position.x, std::min(stick.x, contain.XMost()));
   275   position.x = std::min(position.x, std::max(stick.XMost(), contain.x));
   277   return position;
   278 }
   280 void
   281 StickyScrollContainer::GetScrollRanges(nsIFrame* aFrame, nsRect* aOuter,
   282                                        nsRect* aInner) const
   283 {
   284   // We need to use the first in flow; ComputeStickyLimits requires
   285   // this, at the very least because its call to
   286   // nsLayoutUtils::GetAllInFlowRectsUnion requires it.
   287   nsIFrame *firstCont =
   288     nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame);
   290   nsRect stick;
   291   nsRect contain;
   292   ComputeStickyLimits(firstCont, &stick, &contain);
   294   aOuter->SetRect(nscoord_MIN/2, nscoord_MIN/2, nscoord_MAX, nscoord_MAX);
   295   aInner->SetRect(nscoord_MIN/2, nscoord_MIN/2, nscoord_MAX, nscoord_MAX);
   297   const nsPoint normalPosition = aFrame->GetNormalPosition();
   299   // Bottom and top
   300   if (stick.YMost() != nscoord_MAX/2) {
   301     aOuter->SetTopEdge(contain.y - stick.YMost());
   302     aInner->SetTopEdge(normalPosition.y - stick.YMost());
   303   }
   305   if (stick.y != nscoord_MIN/2) {
   306     aInner->SetBottomEdge(normalPosition.y - stick.y);
   307     aOuter->SetBottomEdge(contain.YMost() - stick.y);
   308   }
   310   // Right and left
   311   if (stick.XMost() != nscoord_MAX/2) {
   312     aOuter->SetLeftEdge(contain.x - stick.XMost());
   313     aInner->SetLeftEdge(normalPosition.x - stick.XMost());
   314   }
   316   if (stick.x != nscoord_MIN/2) {
   317     aInner->SetRightEdge(normalPosition.x - stick.x);
   318     aOuter->SetRightEdge(contain.XMost() - stick.x);
   319   }
   320 }
   322 void
   323 StickyScrollContainer::PositionContinuations(nsIFrame* aFrame)
   324 {
   325   NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame),
   326                "Should be starting from the first continuation");
   327   nsPoint translation = ComputePosition(aFrame) - aFrame->GetNormalPosition();
   329   // Move all continuation frames by the same amount.
   330   for (nsIFrame* cont = aFrame; cont;
   331        cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
   332     cont->SetPosition(cont->GetNormalPosition() + translation);
   333   }
   334 }
   336 void
   337 StickyScrollContainer::UpdatePositions(nsPoint aScrollPosition,
   338                                        nsIFrame* aSubtreeRoot)
   339 {
   340 #ifdef DEBUG
   341   {
   342     nsIFrame* scrollFrameAsFrame = do_QueryFrame(mScrollFrame);
   343     NS_ASSERTION(!aSubtreeRoot || aSubtreeRoot == scrollFrameAsFrame,
   344                  "If reflowing, should be reflowing the scroll frame");
   345   }
   346 #endif
   347   mScrollPosition = aScrollPosition;
   349   OverflowChangedTracker oct;
   350   oct.SetSubtreeRoot(aSubtreeRoot);
   351   for (nsTArray<nsIFrame*>::size_type i = 0; i < mFrames.Length(); i++) {
   352     nsIFrame* f = mFrames[i];
   353     if (!nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(f)) {
   354       // This frame was added in nsFrame::Init before we knew it wasn't
   355       // the first ib-split-sibling.
   356       mFrames.RemoveElementAt(i);
   357       --i;
   358       continue;
   359     }
   361     if (aSubtreeRoot) {
   362       // Reflowing the scroll frame, so recompute offsets.
   363       ComputeStickyOffsets(f);
   364     }
   365     // mFrames will only contain first continuations, because we filter in
   366     // nsIFrame::Init.
   367     PositionContinuations(f);
   369     f = f->GetParent();
   370     if (f != aSubtreeRoot) {
   371       for (nsIFrame* cont = f; cont;
   372            cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
   373         oct.AddFrame(cont, OverflowChangedTracker::CHILDREN_CHANGED);
   374       }
   375     }
   376   }
   377   oct.Flush();
   378 }
   380 void
   381 StickyScrollContainer::ScrollPositionWillChange(nscoord aX, nscoord aY)
   382 {
   383 }
   385 void
   386 StickyScrollContainer::ScrollPositionDidChange(nscoord aX, nscoord aY)
   387 {
   388   UpdatePositions(nsPoint(aX, aY), nullptr);
   389 }
   391 } // namespace mozilla

mercurial