layout/generic/StickyScrollContainer.cpp

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/layout/generic/StickyScrollContainer.cpp	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,391 @@
     1.4 +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim: set ts=2 sts=2 et sw=2 tw=80: */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +
    1.10 +/**
    1.11 + * compute sticky positioning, both during reflow and when the scrolling
    1.12 + * container scrolls
    1.13 + */
    1.14 +
    1.15 +#include "StickyScrollContainer.h"
    1.16 +#include "nsIFrame.h"
    1.17 +#include "nsIScrollableFrame.h"
    1.18 +#include "nsLayoutUtils.h"
    1.19 +#include "RestyleTracker.h"
    1.20 +
    1.21 +using namespace mozilla::css;
    1.22 +
    1.23 +namespace mozilla {
    1.24 +
    1.25 +void DestroyStickyScrollContainer(void* aPropertyValue)
    1.26 +{
    1.27 +  delete static_cast<StickyScrollContainer*>(aPropertyValue);
    1.28 +}
    1.29 +
    1.30 +NS_DECLARE_FRAME_PROPERTY(StickyScrollContainerProperty,
    1.31 +                          DestroyStickyScrollContainer)
    1.32 +
    1.33 +StickyScrollContainer::StickyScrollContainer(nsIScrollableFrame* aScrollFrame)
    1.34 +  : mScrollFrame(aScrollFrame)
    1.35 +  , mScrollPosition()
    1.36 +{
    1.37 +  mScrollFrame->AddScrollPositionListener(this);
    1.38 +}
    1.39 +
    1.40 +StickyScrollContainer::~StickyScrollContainer()
    1.41 +{
    1.42 +  mScrollFrame->RemoveScrollPositionListener(this);
    1.43 +}
    1.44 +
    1.45 +// static
    1.46 +StickyScrollContainer*
    1.47 +StickyScrollContainer::GetStickyScrollContainerForFrame(nsIFrame* aFrame)
    1.48 +{
    1.49 +  nsIScrollableFrame* scrollFrame =
    1.50 +    nsLayoutUtils::GetNearestScrollableFrame(aFrame->GetParent(),
    1.51 +      nsLayoutUtils::SCROLLABLE_SAME_DOC |
    1.52 +      nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
    1.53 +  if (!scrollFrame) {
    1.54 +    // We might not find any, for instance in the case of
    1.55 +    // <html style="position: fixed">
    1.56 +    return nullptr;
    1.57 +  }
    1.58 +  FrameProperties props = static_cast<nsIFrame*>(do_QueryFrame(scrollFrame))->
    1.59 +    Properties();
    1.60 +  StickyScrollContainer* s = static_cast<StickyScrollContainer*>
    1.61 +    (props.Get(StickyScrollContainerProperty()));
    1.62 +  if (!s) {
    1.63 +    s = new StickyScrollContainer(scrollFrame);
    1.64 +    props.Set(StickyScrollContainerProperty(), s);
    1.65 +  }
    1.66 +  return s;
    1.67 +}
    1.68 +
    1.69 +// static
    1.70 +void
    1.71 +StickyScrollContainer::NotifyReparentedFrameAcrossScrollFrameBoundary(nsIFrame* aFrame,
    1.72 +                                                                      nsIFrame* aOldParent)
    1.73 +{
    1.74 +  nsIScrollableFrame* oldScrollFrame =
    1.75 +    nsLayoutUtils::GetNearestScrollableFrame(aOldParent,
    1.76 +      nsLayoutUtils::SCROLLABLE_SAME_DOC |
    1.77 +      nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
    1.78 +  if (!oldScrollFrame) {
    1.79 +    // XXX maybe aFrame has sticky descendants that can be sticky now, but
    1.80 +    // we aren't going to handle that.
    1.81 +    return;
    1.82 +  }
    1.83 +  FrameProperties props = static_cast<nsIFrame*>(do_QueryFrame(oldScrollFrame))->
    1.84 +    Properties();
    1.85 +  StickyScrollContainer* oldSSC = static_cast<StickyScrollContainer*>
    1.86 +    (props.Get(StickyScrollContainerProperty()));
    1.87 +  if (!oldSSC) {
    1.88 +    // aOldParent had no sticky descendants, so aFrame doesn't have any sticky
    1.89 +    // descendants, and we're done here.
    1.90 +    return;
    1.91 +  }
    1.92 +
    1.93 +  auto i = oldSSC->mFrames.Length();
    1.94 +  while (i-- > 0) {
    1.95 +    nsIFrame* f = oldSSC->mFrames[i];
    1.96 +    StickyScrollContainer* newSSC = GetStickyScrollContainerForFrame(f);
    1.97 +    if (newSSC != oldSSC) {
    1.98 +      oldSSC->RemoveFrame(f);
    1.99 +      if (newSSC) {
   1.100 +        newSSC->AddFrame(f);
   1.101 +      }
   1.102 +    }
   1.103 +  }
   1.104 +}
   1.105 +
   1.106 +// static
   1.107 +StickyScrollContainer*
   1.108 +StickyScrollContainer::GetStickyScrollContainerForScrollFrame(nsIFrame* aFrame)
   1.109 +{
   1.110 +  FrameProperties props = aFrame->Properties();
   1.111 +  return static_cast<StickyScrollContainer*>
   1.112 +    (props.Get(StickyScrollContainerProperty()));
   1.113 +}
   1.114 +
   1.115 +static nscoord
   1.116 +ComputeStickySideOffset(Side aSide, const nsStyleSides& aOffset,
   1.117 +                        nscoord aPercentBasis)
   1.118 +{
   1.119 +  if (eStyleUnit_Auto == aOffset.GetUnit(aSide)) {
   1.120 +    return NS_AUTOOFFSET;
   1.121 +  } else {
   1.122 +    return nsLayoutUtils::ComputeCBDependentValue(aPercentBasis,
   1.123 +                                                  aOffset.Get(aSide));
   1.124 +  }
   1.125 +}
   1.126 +
   1.127 +// static
   1.128 +void
   1.129 +StickyScrollContainer::ComputeStickyOffsets(nsIFrame* aFrame)
   1.130 +{
   1.131 +  nsIScrollableFrame* scrollableFrame =
   1.132 +    nsLayoutUtils::GetNearestScrollableFrame(aFrame->GetParent(),
   1.133 +      nsLayoutUtils::SCROLLABLE_SAME_DOC |
   1.134 +      nsLayoutUtils::SCROLLABLE_INCLUDE_HIDDEN);
   1.135 +
   1.136 +  if (!scrollableFrame) {
   1.137 +    // Bail.
   1.138 +    return;
   1.139 +  }
   1.140 +
   1.141 +  nsSize scrollContainerSize = scrollableFrame->GetScrolledFrame()->
   1.142 +    GetContentRectRelativeToSelf().Size();
   1.143 +
   1.144 +  nsMargin computedOffsets;
   1.145 +  const nsStylePosition* position = aFrame->StylePosition();
   1.146 +
   1.147 +  computedOffsets.left   = ComputeStickySideOffset(eSideLeft, position->mOffset,
   1.148 +                                                   scrollContainerSize.width);
   1.149 +  computedOffsets.right  = ComputeStickySideOffset(eSideRight, position->mOffset,
   1.150 +                                                   scrollContainerSize.width);
   1.151 +  computedOffsets.top    = ComputeStickySideOffset(eSideTop, position->mOffset,
   1.152 +                                                   scrollContainerSize.height);
   1.153 +  computedOffsets.bottom = ComputeStickySideOffset(eSideBottom, position->mOffset,
   1.154 +                                                   scrollContainerSize.height);
   1.155 +
   1.156 +  // Store the offset
   1.157 +  FrameProperties props = aFrame->Properties();
   1.158 +  nsMargin* offsets = static_cast<nsMargin*>
   1.159 +    (props.Get(nsIFrame::ComputedOffsetProperty()));
   1.160 +  if (offsets) {
   1.161 +    *offsets = computedOffsets;
   1.162 +  } else {
   1.163 +    props.Set(nsIFrame::ComputedOffsetProperty(),
   1.164 +              new nsMargin(computedOffsets));
   1.165 +  }
   1.166 +}
   1.167 +
   1.168 +void
   1.169 +StickyScrollContainer::ComputeStickyLimits(nsIFrame* aFrame, nsRect* aStick,
   1.170 +                                           nsRect* aContain) const
   1.171 +{
   1.172 +  NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame),
   1.173 +               "Can't sticky position individual continuations");
   1.174 +
   1.175 +  aStick->SetRect(nscoord_MIN/2, nscoord_MIN/2, nscoord_MAX, nscoord_MAX);
   1.176 +  aContain->SetRect(nscoord_MIN/2, nscoord_MIN/2, nscoord_MAX, nscoord_MAX);
   1.177 +
   1.178 +  const nsMargin* computedOffsets = static_cast<nsMargin*>(
   1.179 +    aFrame->Properties().Get(nsIFrame::ComputedOffsetProperty()));
   1.180 +  if (!computedOffsets) {
   1.181 +    // We haven't reflowed the scroll frame yet, so offsets haven't been
   1.182 +    // computed. Bail.
   1.183 +    return;
   1.184 +  }
   1.185 +
   1.186 +  nsIFrame* scrolledFrame = mScrollFrame->GetScrolledFrame();
   1.187 +  nsIFrame* cbFrame = aFrame->GetContainingBlock();
   1.188 +  NS_ASSERTION(cbFrame == scrolledFrame ||
   1.189 +    nsLayoutUtils::IsProperAncestorFrame(scrolledFrame, cbFrame),
   1.190 +    "Scroll frame should be an ancestor of the containing block");
   1.191 +
   1.192 +  nsRect rect =
   1.193 +    nsLayoutUtils::GetAllInFlowRectsUnion(aFrame, aFrame->GetParent());
   1.194 +
   1.195 +  // Containing block limits for the position of aFrame relative to its parent.
   1.196 +  // The margin box of the sticky element stays within the content box of the
   1.197 +  // contaning-block element.
   1.198 +  if (cbFrame != scrolledFrame) {
   1.199 +    *aContain = nsLayoutUtils::
   1.200 +      GetAllInFlowRectsUnion(cbFrame, aFrame->GetParent(),
   1.201 +                             nsLayoutUtils::RECTS_USE_CONTENT_BOX);
   1.202 +    nsRect marginRect = nsLayoutUtils::
   1.203 +      GetAllInFlowRectsUnion(aFrame, aFrame->GetParent(),
   1.204 +                             nsLayoutUtils::RECTS_USE_MARGIN_BOX);
   1.205 +
   1.206 +    // Deflate aContain by the difference between the union of aFrame's
   1.207 +    // continuations' margin boxes and the union of their border boxes, so that
   1.208 +    // by keeping aFrame within aContain, we keep the union of the margin boxes
   1.209 +    // within the containing block's content box.
   1.210 +    aContain->Deflate(marginRect - rect);
   1.211 +
   1.212 +    // Deflate aContain by the border-box size, to form a constraint on the
   1.213 +    // upper-left corner of aFrame and continuations.
   1.214 +    aContain->Deflate(nsMargin(0, rect.width, rect.height, 0));
   1.215 +  }
   1.216 +
   1.217 +  nsMargin sfPadding = scrolledFrame->GetUsedPadding();
   1.218 +  nsPoint sfOffset = aFrame->GetParent()->GetOffsetTo(scrolledFrame);
   1.219 +
   1.220 +  // Top
   1.221 +  if (computedOffsets->top != NS_AUTOOFFSET) {
   1.222 +    aStick->SetTopEdge(mScrollPosition.y + sfPadding.top +
   1.223 +                       computedOffsets->top - sfOffset.y);
   1.224 +  }
   1.225 +
   1.226 +  nsSize sfSize = scrolledFrame->GetContentRectRelativeToSelf().Size();
   1.227 +
   1.228 +  // Bottom
   1.229 +  if (computedOffsets->bottom != NS_AUTOOFFSET &&
   1.230 +      (computedOffsets->top == NS_AUTOOFFSET ||
   1.231 +       rect.height <= sfSize.height - computedOffsets->TopBottom())) {
   1.232 +    aStick->SetBottomEdge(mScrollPosition.y + sfPadding.top + sfSize.height -
   1.233 +                          computedOffsets->bottom - rect.height - sfOffset.y);
   1.234 +  }
   1.235 +
   1.236 +  uint8_t direction = cbFrame->StyleVisibility()->mDirection;
   1.237 +
   1.238 +  // Left
   1.239 +  if (computedOffsets->left != NS_AUTOOFFSET &&
   1.240 +      (computedOffsets->right == NS_AUTOOFFSET ||
   1.241 +       direction == NS_STYLE_DIRECTION_LTR ||
   1.242 +       rect.width <= sfSize.width - computedOffsets->LeftRight())) {
   1.243 +    aStick->SetLeftEdge(mScrollPosition.x + sfPadding.left +
   1.244 +                        computedOffsets->left - sfOffset.x);
   1.245 +  }
   1.246 +
   1.247 +  // Right
   1.248 +  if (computedOffsets->right != NS_AUTOOFFSET &&
   1.249 +      (computedOffsets->left == NS_AUTOOFFSET ||
   1.250 +       direction == NS_STYLE_DIRECTION_RTL ||
   1.251 +       rect.width <= sfSize.width - computedOffsets->LeftRight())) {
   1.252 +    aStick->SetRightEdge(mScrollPosition.x + sfPadding.left + sfSize.width -
   1.253 +                         computedOffsets->right - rect.width - sfOffset.x);
   1.254 +  }
   1.255 +
   1.256 +  // These limits are for the bounding box of aFrame's continuations. Convert
   1.257 +  // to limits for aFrame itself.
   1.258 +  nsPoint frameOffset = aFrame->GetPosition() - rect.TopLeft();
   1.259 +  aStick->MoveBy(frameOffset);
   1.260 +  aContain->MoveBy(frameOffset);
   1.261 +}
   1.262 +
   1.263 +nsPoint
   1.264 +StickyScrollContainer::ComputePosition(nsIFrame* aFrame) const
   1.265 +{
   1.266 +  nsRect stick;
   1.267 +  nsRect contain;
   1.268 +  ComputeStickyLimits(aFrame, &stick, &contain);
   1.269 +
   1.270 +  nsPoint position = aFrame->GetNormalPosition();
   1.271 +
   1.272 +  // For each sticky direction (top, bottom, left, right), move the frame along
   1.273 +  // the appropriate axis, based on the scroll position, but limit this to keep
   1.274 +  // the element's margin box within the containing block.
   1.275 +  position.y = std::max(position.y, std::min(stick.y, contain.YMost()));
   1.276 +  position.y = std::min(position.y, std::max(stick.YMost(), contain.y));
   1.277 +  position.x = std::max(position.x, std::min(stick.x, contain.XMost()));
   1.278 +  position.x = std::min(position.x, std::max(stick.XMost(), contain.x));
   1.279 +
   1.280 +  return position;
   1.281 +}
   1.282 +
   1.283 +void
   1.284 +StickyScrollContainer::GetScrollRanges(nsIFrame* aFrame, nsRect* aOuter,
   1.285 +                                       nsRect* aInner) const
   1.286 +{
   1.287 +  // We need to use the first in flow; ComputeStickyLimits requires
   1.288 +  // this, at the very least because its call to
   1.289 +  // nsLayoutUtils::GetAllInFlowRectsUnion requires it.
   1.290 +  nsIFrame *firstCont =
   1.291 +    nsLayoutUtils::FirstContinuationOrIBSplitSibling(aFrame);
   1.292 +
   1.293 +  nsRect stick;
   1.294 +  nsRect contain;
   1.295 +  ComputeStickyLimits(firstCont, &stick, &contain);
   1.296 +
   1.297 +  aOuter->SetRect(nscoord_MIN/2, nscoord_MIN/2, nscoord_MAX, nscoord_MAX);
   1.298 +  aInner->SetRect(nscoord_MIN/2, nscoord_MIN/2, nscoord_MAX, nscoord_MAX);
   1.299 +
   1.300 +  const nsPoint normalPosition = aFrame->GetNormalPosition();
   1.301 +
   1.302 +  // Bottom and top
   1.303 +  if (stick.YMost() != nscoord_MAX/2) {
   1.304 +    aOuter->SetTopEdge(contain.y - stick.YMost());
   1.305 +    aInner->SetTopEdge(normalPosition.y - stick.YMost());
   1.306 +  }
   1.307 +
   1.308 +  if (stick.y != nscoord_MIN/2) {
   1.309 +    aInner->SetBottomEdge(normalPosition.y - stick.y);
   1.310 +    aOuter->SetBottomEdge(contain.YMost() - stick.y);
   1.311 +  }
   1.312 +
   1.313 +  // Right and left
   1.314 +  if (stick.XMost() != nscoord_MAX/2) {
   1.315 +    aOuter->SetLeftEdge(contain.x - stick.XMost());
   1.316 +    aInner->SetLeftEdge(normalPosition.x - stick.XMost());
   1.317 +  }
   1.318 +
   1.319 +  if (stick.x != nscoord_MIN/2) {
   1.320 +    aInner->SetRightEdge(normalPosition.x - stick.x);
   1.321 +    aOuter->SetRightEdge(contain.XMost() - stick.x);
   1.322 +  }
   1.323 +}
   1.324 +
   1.325 +void
   1.326 +StickyScrollContainer::PositionContinuations(nsIFrame* aFrame)
   1.327 +{
   1.328 +  NS_ASSERTION(nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(aFrame),
   1.329 +               "Should be starting from the first continuation");
   1.330 +  nsPoint translation = ComputePosition(aFrame) - aFrame->GetNormalPosition();
   1.331 +
   1.332 +  // Move all continuation frames by the same amount.
   1.333 +  for (nsIFrame* cont = aFrame; cont;
   1.334 +       cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
   1.335 +    cont->SetPosition(cont->GetNormalPosition() + translation);
   1.336 +  }
   1.337 +}
   1.338 +
   1.339 +void
   1.340 +StickyScrollContainer::UpdatePositions(nsPoint aScrollPosition,
   1.341 +                                       nsIFrame* aSubtreeRoot)
   1.342 +{
   1.343 +#ifdef DEBUG
   1.344 +  {
   1.345 +    nsIFrame* scrollFrameAsFrame = do_QueryFrame(mScrollFrame);
   1.346 +    NS_ASSERTION(!aSubtreeRoot || aSubtreeRoot == scrollFrameAsFrame,
   1.347 +                 "If reflowing, should be reflowing the scroll frame");
   1.348 +  }
   1.349 +#endif
   1.350 +  mScrollPosition = aScrollPosition;
   1.351 +
   1.352 +  OverflowChangedTracker oct;
   1.353 +  oct.SetSubtreeRoot(aSubtreeRoot);
   1.354 +  for (nsTArray<nsIFrame*>::size_type i = 0; i < mFrames.Length(); i++) {
   1.355 +    nsIFrame* f = mFrames[i];
   1.356 +    if (!nsLayoutUtils::IsFirstContinuationOrIBSplitSibling(f)) {
   1.357 +      // This frame was added in nsFrame::Init before we knew it wasn't
   1.358 +      // the first ib-split-sibling.
   1.359 +      mFrames.RemoveElementAt(i);
   1.360 +      --i;
   1.361 +      continue;
   1.362 +    }
   1.363 +
   1.364 +    if (aSubtreeRoot) {
   1.365 +      // Reflowing the scroll frame, so recompute offsets.
   1.366 +      ComputeStickyOffsets(f);
   1.367 +    }
   1.368 +    // mFrames will only contain first continuations, because we filter in
   1.369 +    // nsIFrame::Init.
   1.370 +    PositionContinuations(f);
   1.371 +
   1.372 +    f = f->GetParent();
   1.373 +    if (f != aSubtreeRoot) {
   1.374 +      for (nsIFrame* cont = f; cont;
   1.375 +           cont = nsLayoutUtils::GetNextContinuationOrIBSplitSibling(cont)) {
   1.376 +        oct.AddFrame(cont, OverflowChangedTracker::CHILDREN_CHANGED);
   1.377 +      }
   1.378 +    }
   1.379 +  }
   1.380 +  oct.Flush();
   1.381 +}
   1.382 +
   1.383 +void
   1.384 +StickyScrollContainer::ScrollPositionWillChange(nscoord aX, nscoord aY)
   1.385 +{
   1.386 +}
   1.387 +
   1.388 +void
   1.389 +StickyScrollContainer::ScrollPositionDidChange(nscoord aX, nscoord aY)
   1.390 +{
   1.391 +  UpdatePositions(nsPoint(aX, aY), nullptr);
   1.392 +}
   1.393 +
   1.394 +} // namespace mozilla

mercurial